
# Copyright (C) 2026 [Massimo Sabattini]
# *
# * Questo programma è software libero; puoi ridistribuirlo e/o modificarlo
# * secondo i termini della GNU General Public License come pubblicato dalla
# * Free Software Foundation; o versione 3 della Licenza, o (a tua scelta)
# * qualsiasi versione successiva.
# *
# * Questo programma è distribuito nella speranza che sia utile,
# * ma SENZA ALCUNA GARANZIA; senza neanche la garanzia implicita di
# * COMMERCIABILITÀ o IDONEITÀ PER UN PARTICOLARE SCOPO.
# * Vedi la GNU General Public License per maggiori dettagli.
# *
# * Dovresti aver ricevuto una copia della GNU General Public License
# * insieme a questo programma; se non l'hai ricevuta, vedi <https://www.gnu.org/licenses>.
# *
# main.py
import sys
import json, os, tempfile, webbrowser
from PySide6.QtWidgets import QMenu
from PySide6.QtCore import Qt, QUrl, QByteArray, QBuffer, QIODevice
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
from PySide6.QtGui import QAction
from PySide6.QtGui import QShortcut, QKeySequence
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtGui import QPixmap, QFont
from PySide6.QtWidgets import QLabel, QFrame, QHBoxLayout, QLineEdit, QFileDialog, QSizePolicy, QScrollArea, QTabWidget
from PySide6.QtGui import QIcon, QGuiApplication, QImage
from PySide6.QtWidgets import QInputDialog
from PySide6.QtCore import QEvent, QTimer
from PySide6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QToolButton, QLayout, QLayoutItem
from PySide6.QtCore import QSize, QPoint, QRect
from PySide6.QtCore import QEventLoop
from PySide6.QtGui import QPainter, QPen, QColor, QPolygon
from PySide6.QtWidgets import QTextEdit
from PySide6.QtGui import QTextOption
from PySide6.QtGui import QTransform
from PySide6.QtCore import QRectF, QPointF
from PySide6.QtPrintSupport import QPrinter, QPrintDialog
from PySide6.QtGui import QDesktopServices, QAction
from PySide6.QtGui import QRegion
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QCheckBox
from PySide6.QtGui import QPdfWriter, QPageSize
from PySide6.QtCore import QLocale
from PySide6.QtWidgets import QScroller
import shutil
from pathlib import Path
import os, re
import math
from math import radians, cos, sin, asin, sqrt
from PySide6.QtCore import QUrl
import json

APP_VERSION = "2.2.9"                                             # VERSIONE aggiorna qui + nel file .iss

# === ICONS PERSISTENCE ===
APPDATA = os.getenv("APPDATA") or os.path.join(os.path.expanduser("~"), "AppData", "Roaming")
USER_ICONS = Path(APPDATA) / "RoadbookEditor" / "Icons"

def ensure_user_icons():
    USER_ICONS.mkdir(parents=True, exist_ok=True)

def add_user_icon(src_path: str) -> str:
    r"""Copia l’icona in %APPDATA%\RoadbookEditor\Icons e ritorna solo il nome file."""
    ensure_user_icons()
    name = Path(src_path).name
    dst = USER_ICONS / name
    try:
        if Path(src_path) != dst:
            shutil.copy2(src_path, dst)
    except Exception:
        pass
    return name  # questo va nel JSON

def resolve_icon_path(name: str) -> str | None:
    r"""Risolvi il percorso reale cercando in USER, {app}\Icons, {app}\_internal\Icons."""
    if not name:
        return None
    candidates = [
        USER_ICONS / name,
        Path(getattr(sys, "_MEIPASS", os.getcwd())) / "Icons" / name,
        Path(getattr(sys, "_MEIPASS", os.getcwd())) / "_internal" / "Icons" / name,
    ]
    for p in candidates:
        if p.exists():
            return str(p)
    return None
# === /ICONS PERSISTENCE ===

#                                                                                                    INTERNAZIONALIZZAZIONE
TAB_KEYS = {"t": "tab.terrain", "a": "tab.alerts", "i": "tab.info", "p": "tab.poi", "s": "tab.signs"}

I18N = {
    "it": {
        "menu.languages": "Lingue",
        "lang.it": "Italiano",
        "lang.es": "Español",
        "lang.en": "English",

        "menu.print": "Stampa PDF",
        "action.create_pdf": "Crea PDF",
    },
    "es": {
        "menu.languages": "Idiomas",
        "lang.it": "Italiano",
        "lang.es": "Español",
        "lang.en": "English",

        "menu.print": "Imprimir PDF",
        "action.create_pdf": "Crear PDF",
    },
    "en": {
        "menu.languages": "Languages",
        "lang.it": "Italian",
        "lang.es": "Spanish",
        "lang.en": "English",

        "menu.print": "Print PDF",
        "action.create_pdf": "Create PDF",
    },
}

I18N["it"].update({
    "status.icon_deleted": "Icona eliminata.",
    "status.icon_flipped": "Icona ribaltata (Flip H).",
    "status.select_icon_first": "Seleziona prima un'icona da ribaltare.",
    "status.pdf_working": "Creazione PDF in corso… potrebbe richiedere ~20 secondi.",
    "status.pdf_saved": "PDF salvato in: {path}",
    "info.save_before_print": "Per includere il titolo corretto, salva il roadbook prima di stampare.",
    "status.roadbook_saved": "Roadbook salvato.",
    "status.opened": "Aperto: {name}",
})

I18N["it"].update({
    "action.check_updates": "Controlla aggiornamenti",
    "updates.title": "Controlla aggiornamenti",
    "updates.text": "Versione installata: {ver}\n\nPer verificare se esiste una versione più recente, apri la pagina ufficiale.",
    "updates.open_page": "Apri pagina",
    "updates.close": "Chiudi",
})

I18N["es"].update({
    "action.check_updates": "Buscar actualizaciones",
    "updates.title": "Buscar actualizaciones",
    "updates.text": "Versión instalada: {ver}\n\nPara comprobar si hay una versión más reciente, abre la página oficial.",
    "updates.open_page": "Abrir página",
    "updates.close": "Cerrar",
})

I18N["en"].update({
    "action.check_updates": "Check for updates",
    "updates.title": "Check for updates",
    "updates.text": "Installed version: {ver}\n\nTo check whether a newer version exists, open the official page.",
    "updates.open_page": "Open page",
    "updates.close": "Close",
})

I18N["es"].update({
    "status.icon_deleted": "Icono eliminado.",
    "status.icon_flipped": "Icono volteado (Flip H).",
    "status.select_icon_first": "Selecciona un icono para voltear.",
    "status.pdf_working": "Creando PDF… puede tardar ~20 segundos.",
    "status.pdf_saved": "PDF guardado en: {path}",
    "info.save_before_print": "Para incluir el título correcto, guarda el roadbook antes de imprimir.",
    "status.roadbook_saved": "Roadbook guardado.",
    "status.opened": "Abierto: {name}",
})

I18N["en"].update({
    "status.icon_deleted": "Icon deleted.",
    "status.icon_flipped": "Icon flipped (Flip H).",
    "status.select_icon_first": "Select an icon to flip.",
    "status.pdf_working": "Creating PDF… this may take ~20 seconds.",
    "status.pdf_saved": "PDF saved to: {path}",
    "info.save_before_print": "To include the correct title, save the roadbook before printing.",
    "status.roadbook_saved": "Roadbook saved.",
    "status.opened": "Opened: {name}",
})
I18N["it"].update({
    "menu.track": "Traccia",
    "action.load_gpx": "Carica GPX",
    "action.reduce_track_points": "Riduci i punti della traccia",
    "action.invert_track": "Inverti la traccia",
})
I18N["es"].update({
    "menu.track": "Ruta",
    "action.load_gpx": "Cargar GPX",
    "action.reduce_track_points": "Reducir puntos de la ruta",
    "action.invert_track": "Invertir la ruta",
})
I18N["en"].update({
    "menu.track": "Track",
    "action.load_gpx": "Load GPX",
    "action.reduce_track_points": "Reduce track points",
    "action.invert_track": "Invert track",
})
I18N["it"].update({
    "menu.edit_points": "Edita punti",
    "action.add_wp": "Aggiungi waypoint",
    "action.move_pt": "Muovi punto",
    "action.add_mid_pt": "Aggiungi punto intermedio",
    "action.add_on_line": "Aggiungi punto su linea",
    "action.wp_to_tp": "Trasforma waypoint in trackpoint",
    "action.del_pt": "Cancella punto",
})

I18N["es"].update({
    "menu.edit_points": "Editar puntos",
    "action.add_wp": "Añadir waypoint",
    "action.move_pt": "Mover punto",
    "action.add_mid_pt": "Añadir punto intermedio",
    "action.add_on_line": "Añadir punto en la línea",
    "action.wp_to_tp": "Convertir waypoint en trackpoint",
    "action.del_pt": "Eliminar punto",
})

I18N["en"].update({
    "menu.edit_points": "Edit points",
    "action.add_wp": "Add waypoint",
    "action.move_pt": "Move point",
    "action.add_mid_pt": "Add intermediate point",
    "action.add_on_line": "Add point on line",
    "action.wp_to_tp": "Convert waypoint to trackpoint",
    "action.del_pt": "Delete point",
})
I18N["it"].update({
    "tab.terrain": "TERRENO",
    "tab.alerts":  "AVVISI",
    "tab.info":    "INFO!",
    "tab.poi":     "POI",
    "tab.signs":   "SEGNALI",
})
I18N["es"].update({
    "tab.terrain": "TERRENO",
    "tab.alerts":  "AVISOS",
    "tab.info":    "INFO!",
    "tab.poi":     "POI",
    "tab.signs":   "SEÑALES",
})
I18N["en"].update({
    "tab.terrain": "TERRAIN",
    "tab.alerts":  "ALERTS",
    "tab.info":    "INFO!",
    "tab.poi":     "POI",
    "tab.signs":   "SIGNS",
})
I18N["it"].update({
    "tip.flip_h": "Flip H",
    "tip.delete": "Elimina selezionato",
    "tip.highway": "Autostrada",
    "tip.asphalt": "Asfalto",
    "tip.dirt": "Pista",
    "tip.offroad": "Fuoripista",
    "tip.add_bivio": "Aggiungi bivio",
    "tip.insert_cap": "Inserisci CAP",
    "tip.insert_text": "Inserisci testo",
})
I18N["es"].update({
    "tip.flip_h": "Voltear H",
    "tip.delete": "Eliminar seleccionado",
    "tip.highway": "Autopista",
    "tip.asphalt": "Asfalto",
    "tip.dirt": "Pista",
    "tip.offroad": "Off-road",
    "tip.add_bivio": "Añadir cruce",
    "tip.insert_cap": "Insertar CAP",
    "tip.insert_text": "Insertar texto",
})
I18N["en"].update({
    "tip.flip_h": "Flip H",
    "tip.delete": "Delete selected",
    "tip.highway": "Highway",
    "tip.asphalt": "Asphalt",
    "tip.dirt": "Dirt track",
    "tip.offroad": "Off-road",
    "tip.add_bivio": "Add junction",
    "tip.insert_cap": "Insert CAP",
    "tip.insert_text": "Insert text",
})
TOOLTIP_KEYS = {
    "porta_sotto_push_button": "tip.flip_h",
    "delete_push_button":      "tip.delete",
    "highway_road_type":       "tip.highway",
    "asphalt_road_type":       "tip.asphalt",
    "dirt_track_road_type":    "tip.dirt",
    "off_road_type":           "tip.offroad",
    "bivio_push_button":       "tip.add_bivio",
    "cap_push_button":         "tip.insert_cap",
    "text_push_button":        "tip.insert_text",
}

I18N["it"].update({"msg.title.info": "Informazioni"})
I18N["es"].update({"msg.title.info": "Información"})
I18N["en"].update({"msg.title.info": "Info"})
I18N["it"].update({"status.notes": "Note: {n}"})
I18N["es"].update({"status.notes": "Notas: {n}"})
I18N["en"].update({"status.notes": "Notes: {n}"})
I18N["it"].update({"msg.title.error": "Errore"})
I18N["es"].update({"msg.title.error": "Error"})
I18N["en"].update({"msg.title.error": "Error"})
I18N["it"].update({"error.open": "Errore apertura roadbook: {err}"})
I18N["es"].update({"error.open": "Error al abrir el roadbook: {err}"})
I18N["en"].update({"error.open": "Error opening roadbook: {err}"})
I18N["it"].update({"error.save": "Errore salvataggio roadbook: {err}"})
I18N["es"].update({"error.save": "Error al guardar el roadbook: {err}"})
I18N["en"].update({"error.save": "Error saving roadbook: {err}"})
I18N["it"].update({"error.cap": "Errore nel calcolo del CAP."})
I18N["es"].update({"error.cap": "Error en el cálculo del CAP."})
I18N["en"].update({"error.cap": "Error calculating CAP."})
I18N["it"].update({"menu.file": "File"})
I18N["es"].update({"menu.file": "Archivo"})
I18N["en"].update({"menu.file": "File"})
I18N["it"].update({"menu.open": "Apri…"})
I18N["es"].update({"menu.open": "Abrir…"})
I18N["en"].update({"menu.open": "Open…"})
I18N["it"].update({"menu.save_rb": "Salva roadbook…"})
I18N["es"].update({"menu.save_rb": "Guardar roadbook…"})
I18N["en"].update({"menu.save_rb": "Save roadbook…"})
I18N["it"].update({"menu.export_digital": "Esporta digitale"})
I18N["es"].update({"menu.export_digital": "Exportar digital"})
I18N["en"].update({"menu.export_digital": "Export digital"})
I18N["it"].update({"menu.exit": "Esci"})
I18N["es"].update({"menu.exit": "Salir"})
I18N["en"].update({"menu.exit": "Exit"})
I18N["it"].update({"menu.save_track_gpx": "Salva la sola traccia"})
I18N["es"].update({"menu.save_track_gpx": "Guardar solo la ruta"})
I18N["en"].update({"menu.save_track_gpx": "Save track only"})
I18N["it"].update({"menu.help": "Aiuto"})
I18N["es"].update({"menu.help": "Ayuda"})
I18N["en"].update({"menu.help": "Help"})
I18N["it"].update({"help.open_it": "Apri guida (Italiano)"})
I18N["es"].update({"help.open_it": "Abrir guía (Italiano)"})
I18N["en"].update({"help.open_it": "Open guide (Italian)"})
I18N["it"].update({"menu.map": "Mappa", "action.labels": "Satellite-->etichette on/off", "action.map_outdoor": "Outdoor"})
I18N["es"].update({"menu.map": "Mapa",  "action.labels": "Satélit-->etiquetas on/off", "action.map_outdoor": "Outdoor"})
I18N["en"].update({"menu.map": "Map",   "action.labels": "Satellite-->labels on/off",    "action.map_outdoor": "Outdoors"})
I18N["it"].update({
    "map.open_earth": "Apri in Google Earth Pro",
    "map.open_wayback": "Apri in ArcGIS Wayback",
    "map.open_gmaps": "Apri in Google Maps",
    "map.copy_coords": "Copia coordinate",
})
I18N["es"].update({
    "map.open_earth": "Abrir en Google Earth Pro",
    "map.open_wayback": "Abrir en ArcGIS Wayback",
    "map.open_gmaps": "Abrir en Google Maps",
    "map.copy_coords": "Copiar coordenadas",
})
I18N["en"].update({
    "map.open_earth": "Open in Google Earth Pro",
    "map.open_wayback": "Open in ArcGIS Wayback",
    "map.open_gmaps": "Open in Google Maps",
    "map.copy_coords": "Copy coordinates",
})
I18N["it"].update({"menu.edit_points": "Edita punti"})
I18N["es"].update({"menu.edit_points": "Editar puntos"})
I18N["en"].update({"menu.edit_points": "Edit points"})
I18N["it"].update({"ui.title_placeholder": "Inserire un nome per il RoadBook"})
I18N["es"].update({"ui.title_placeholder": "Introduce un nombre para el RoadBook"})
I18N["en"].update({"ui.title_placeholder": "Enter a name for the RoadBook"})


#                                                                                                       DIZIONARIO TIPO DI STRADA modificare qui gli spessori e colori
road_types = [
    {"type": 0, "name": "Default", "color": "#dcdcdc", "lines": 1, "thickness": 5.00},
    {"type": 1, "name": "Autostrada", "color": "blue", "lines": 2, "thickness": 12.00, "center_thickness": 5.00},
    {"type": 2, "name": "Strada asfaltata", "color": "blue", "lines": 2, "thickness": 8.00, "center_thickness": 2.50},
    {"type": 3, "name": "Pista", "color": "black", "lines": 1, "thickness": 3.5},
    {"type": 4, "name": "Fuoripista", "color": "red", "lines": 1, "thickness": 3.5, "dashed": True}
]

#                                                                                                                            CLASS MAINWINDOW
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("RoadBook Editor powered by Max&ChatGPT")
        self.resize(1400, 900)
        self.file_modificato_flag = False  # <--                                      Flag che indica se database_traccia è stato modificato o no
        self.soglia_rdp_default = 10.0 #                                         soglia di attuazione per il logaritmo RDP di riduci punti, default 10.
        self.nome_file_gpx = "" # restituisce il nome del file gpx caricato
        self.num_punti_traccia = 0 # restituisce il valore attualizzato dei punti della traccia
        self.act_riduci_punti_rdp = None  # referenza all'azione di menu (la settiamo dopo)
        # ---                                                                                               ¡  Parametri NOTE ----------------------------------------------------------
        # Altezza di ogni nota (px) — modificabile da codice
        self.NOTE_H = 160
        # Larghezze iniziali colonne A e B; la C si espande
        self.COL_A_W = 140
        self.COL_B_W = 260
        # Soglia parziale (km) per evidenziare la colonna A in azzurrino
        self.SOGLIA_PARZIALE_SHORT_KM = 0.25
        # Font size (pt) per progressivo, parziale, numero nota
        self.FONT_PROG = 28
        self.FONT_PARZ = 20
        self.FONT_NUM  = 18
        self.FONT_CAP_BOTTOM   = 14   # CAP basso (pt)
        self.CAP_HDR_H = 32        # altezza slot CAP rosso (px)
        # (già presente) self.FONT_CAP_HDR_SCALE = 0.80
        self.FONT_COORD_BOTTOM = 14   # Coordinate basso (pt)
        self.FONT_CAP_HDR_SCALE = 0.70   # 70% del base
        self.TEXT_MIN_H = 72  # altezza minima area testo (px)
        self.TEXT_MAX_LEN = 50
        self._selected_vector_num = None
        self._recent_note_num = None
        self._selected_bivio = None
        self._current_note_num = None
        self._init_i18n()
        self.retranslate_ui()
        self.roadbook_caricato = False

        #                                                                                                             MENU
        # BARRA DEL MENU ---
        menubar = self.menuBar()

        #                                                                                                           MENU FILE
        file_menu = self.menu_file = menubar.addMenu(self.t("menu.file"))

        # Apri un RoadBook
        self.act_apri_roadbook = QAction(self.t("menu.open"), self)
        self.act_apri_roadbook.setShortcut(QKeySequence.Open)  # oppure "Ctrl+O"
        self.act_apri_roadbook.triggered.connect(self.apri_roadbook)
        file_menu.addAction(self.act_apri_roadbook)

        # Salva roadbook…
        self.act_salva_roadbook = QAction(self.t("menu.save_rb"), self)
        self.act_salva_roadbook.setShortcut(QKeySequence.Save)  # Ctrl+S
        self.act_salva_roadbook.triggered.connect(self.salva_roadbook)
        file_menu.addAction(self.act_salva_roadbook)        

        file_menu.addSeparator()

        # Esporta digitale (pacchetto .zip per RB Reader)
        self.act_esporta_digitale = QAction(self.t("menu.export_digital"), self)
        self.act_esporta_digitale.setShortcut("Ctrl+D")
        self.act_esporta_digitale.triggered.connect(self.esporta_digitale)  # funzione che aggiungeremo dopo
        file_menu.addAction(self.act_esporta_digitale)

        # Chiudi il programma
        self.act_exit = QAction(self.t("menu.exit"), self)
        self.act_exit.setShortcut(QKeySequence.Quit)  # oppure "Ctrl+Q"
        self.act_exit.triggered.connect(self.close_program)
        file_menu.addAction(self.act_exit)

        #                                                                                                              MENU MAPPA
        self.menu_map = menubar.addMenu(self.t("menu.map"))

        # Toggle etichette
        self.act_toggle_etichetta = QAction(self.t("action.labels"), self)
        self.act_toggle_etichetta.setShortcut("Ctrl+0")
        self.act_toggle_etichetta.setCheckable(True)
        self.act_toggle_etichetta.setChecked(True)
        self.act_toggle_etichetta.triggered.connect(self.toggle_etichetta)
        self.menu_map.addAction(self.act_toggle_etichetta)
        self.menu_map.addSeparator()

        self.act_map_outdoor = QAction(self.t("action.map_outdoor"), self)
        self.act_map_outdoor.setShortcut("Ctrl+1")
        self.act_map_outdoor.setShortcutContext(Qt.ApplicationShortcut)
        self.act_map_outdoor.triggered.connect(self.mappa_outdoor)
        self.menu_map.addAction(self.act_map_outdoor)

        #                                                                                                             MENU TRACCIA
        self.menu_track = menubar.addMenu(self.t("menu.track"))

        # Carica un file.gpx
        self.act_load_gpx = QAction(self.t("action.load_gpx"), self)
        self.act_load_gpx.setShortcut("Ctrl+X")
        self.act_load_gpx.triggered.connect(self.carica_gpx)
        self.menu_track.addAction(self.act_load_gpx)

        # Riduci i punti di una traccia
        self.act_riduci_punti_rdp = QAction(self.t("action.reduce_track_points"), self)
        self.act_riduci_punti_rdp.setShortcut("Ctrl+R")
        self.act_riduci_punti_rdp.triggered.connect(lambda: self.riduci_punti_rdp(self.soglia_rdp_default))
        self.menu_track.addAction(self.act_riduci_punti_rdp)
        self.act_riduci_punti_rdp.setEnabled(False)

        # Inverti un file.gpx
        self.act_inverti_gpx = QAction(self.t("action.invert_track"), self)
        self.act_inverti_gpx.setShortcut("Ctrl+I")
        self.act_inverti_gpx.triggered.connect(self.inverti_gpx)
        self.menu_track.addAction(self.act_inverti_gpx)
        self.act_inverti_gpx.setEnabled(False)

        # Salva la traccia in un file.gpx
        self.act_salva_gpx = QAction(self.t("menu.save_track_gpx"), self)
        self.act_salva_gpx.setShortcut("Ctrl+G")
        self.act_salva_gpx.triggered.connect(self.salva_gpx)
        self.menu_track.addAction(self.act_salva_gpx)

        #                                                                                                   LINEA PER DEBUG DATABASE_TRACCIA
        # Scorciatoia per mostrare nel terminale il database traccia
        shortcut_show_db = QShortcut(QKeySequence("Ctrl+5"), self)
        shortcut_show_db.activated.connect(self.mostra_database_traccia)

        #                                                                                                       MENU EDITA PUNTI TRACCIA ---
        self.menu_edit_points = menubar.addMenu(self.t("menu.edit_points"))

        # AGGIUNGI WAYPOINT  (W)
        self.act_aggiungi_waypoint = QAction(self.t("action.add_wp"), self)
        self.act_aggiungi_waypoint.setShortcut("W")
        self.act_aggiungi_waypoint.triggered.connect(self.aggiungi_waypoint)
        self.act_aggiungi_waypoint.setEnabled(False)
        self.menu_edit_points.addAction(self.act_aggiungi_waypoint)

        # MUOVI PUNTO  (M)
        self.act_muovi_punto = QAction(self.t("action.move_pt"), self)
        self.act_muovi_punto.setShortcut("M")
        self.act_muovi_punto.triggered.connect(self.muovi_punto)
        self.act_muovi_punto.setEnabled(False)
        self.menu_edit_points.addAction(self.act_muovi_punto)

        # AGGIUNGI PUNTO INTERMEDIO  (A)
        self.act_aggiungi_intermedio = QAction(self.t("action.add_mid_pt"), self)
        self.act_aggiungi_intermedio.setShortcut("A")
        self.act_aggiungi_intermedio.triggered.connect(self.aggiungi_punto_intermedio)
        self.act_aggiungi_intermedio.setEnabled(False)
        self.menu_edit_points.addAction(self.act_aggiungi_intermedio)

        # AGGIUNGI PUNTO SU LINEA  (L)
        self.act_aggiungi_su_linea = QAction(self.t("action.add_on_line"), self)
        self.act_aggiungi_su_linea.setShortcut("L")
        self.act_aggiungi_su_linea.triggered.connect(self.aggiungi_punto_su_linea)
        self.act_aggiungi_su_linea.setEnabled(False)
        self.menu_edit_points.addAction(self.act_aggiungi_su_linea)

        # TRASFORMA WAYPOINT IN TRACKPOINT  (T)
        self.act_wp_to_tp = QAction(self.t("action.wp_to_tp"), self)
        self.act_wp_to_tp.setShortcut("T")
        self.act_wp_to_tp.triggered.connect(self.trasforma_waypoint_in_trackpoint)
        self.act_wp_to_tp.setEnabled(False)
        self.menu_edit_points.addAction(self.act_wp_to_tp)

        # CANCELLA PUNTO  (Del)
        self.act_cancella_punto = QAction(self.t("action.del_pt"), self)
        self.act_cancella_punto.setShortcut("Del")
        self.act_cancella_punto.triggered.connect(self.cancella_punto)
        self.act_cancella_punto.setEnabled(False)
        self.menu_edit_points.addAction(self.act_cancella_punto)

        #                                                                                                          MENU CREA PDF ---
        # --- Menu Stampa PDF (localizzato)
        self.menu_print = menubar.addMenu(self.t("menu.print"))

        self.act_crea_pdf = QAction(self.t("action.create_pdf"), self)
        self.act_crea_pdf.setShortcut(QKeySequence.Print)
        self.act_crea_pdf.setShortcutContext(Qt.ApplicationShortcut)
        self.act_crea_pdf.triggered.connect(self.crea_pdf)
        self.menu_print.addAction(self.act_crea_pdf)
        self.addAction(self.act_crea_pdf)

        #                                                                                                          MENU LINGUE
        # --- Menu Lingue
        from PySide6.QtGui import QActionGroup
        self.menu_languages = menubar.addMenu(self.t("menu.languages"))

        grp = QActionGroup(self)
        grp.setExclusive(True)

        self.act_lang_it = QAction(self.t("lang.it"), self, checkable=True)
        self.act_lang_es = QAction(self.t("lang.es"), self, checkable=True)
        self.act_lang_en = QAction(self.t("lang.en"), self, checkable=True)

        self.menu_languages.addActions([self.act_lang_it, self.act_lang_es, self.act_lang_en])
        grp.addAction(self.act_lang_it); grp.addAction(self.act_lang_es); grp.addAction(self.act_lang_en)

        # stato iniziale
        cur = getattr(self, "current_lang", "en")
        self.act_lang_it.setChecked(cur == "it")
        self.act_lang_es.setChecked(cur == "es")
        self.act_lang_en.setChecked(cur == "en")

        # switch lingua
        self.act_lang_it.triggered.connect(lambda: self.set_language("it"))
        self.act_lang_es.triggered.connect(lambda: self.set_language("es"))
        self.act_lang_en.triggered.connect(lambda: self.set_language("en"))

        #                                                                                                            MENU HELP
        self.menu_help = self.menuBar().addMenu("")  # titolo via retranslate_ui

        self.act_help_it = QAction(self)
        self.act_help_it.triggered.connect(lambda: self._open_pdf("Aiuto.pdf"))
        self.menu_help.addAction(self.act_help_it)

        self.act_help_es = QAction(self)
        self.act_help_es.triggered.connect(lambda: self._open_pdf("Ayuda.pdf"))
        self.menu_help.addAction(self.act_help_es)

        self.act_help_en = QAction(self)
        self.act_help_en.triggered.connect(lambda: self._open_pdf("Help.pdf"))
        self.menu_help.addAction(self.act_help_en)

        # aggiornamenti
        self.menu_help.addSeparator()
        self.act_updates = QAction(self)
        self.act_updates.triggered.connect(self.controlla_aggiornamenti)
        self.menu_help.addAction(self.act_updates)

        # imposta i testi localizzati
        self.retranslate_ui()

        #                                                                                                            STATUS BAR
        # --- STATUS BAR ---
        self.statusBar().showMessage("Pronto")

        #                                                                                                 DISEGNO DELLA FINESTRA PRINCIPALE
        # --- LAYOUT (2/3 – 1/3) ---
        central = QWidget()
        root_h = QHBoxLayout(central)
        root_h.setContentsMargins(0, 0, 0, 0)
        root_h.setSpacing(0)

        #                                                                                                            AREA MAPPA
        # Sinistra: mappa da file HTML
        self.web = QWebEngineView()

        # MENU contestuale: lo gestiamo via eventFilter (QWebEngine+Mapbox è capriccioso)
        self.web.setContextMenuPolicy(Qt.NoContextMenu)

        self.web.setMinimumWidth(220)
        self.web.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        map_path = os.path.join(os.path.dirname(__file__), "mappa.html")
        with open(map_path, "r", encoding="utf-8") as f:
            html_content = f.read()
        self.web.setHtml(html_content, QUrl("https://unpkg.com/"))
        root_h.addWidget(self.web)

        #                                                                                                           AREA ROADBOOK
        # Destra: AREA ROADBOOK (fissa in alto)
        right = QWidget()
        right.setStyleSheet("background:#ffffff;")
        layout_right = QVBoxLayout(right)
        layout_right.setContentsMargins(0, 0, 0, 0)
        layout_right.setSpacing(0)

        right.setMinimumWidth(120 + 1 + 220 + 1 + 240 + 20)  # = 602
        right.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        self.right = right  # per resize, se lo usi più avanti

        # Aggiungi il pannello destro al layout orizzontale
        root_h.addWidget(right)

        # Rapporto 2:1 stabile ai resize
        root_h.setStretch(0, 2)  # mappa
        root_h.setStretch(1, 1)  # roadbook

        # Imposta il centrale
        self.setCentralWidget(central)
        QApplication.instance().installEventFilter(self)

        #                                                                                        MODIFICARE PER ALTEZZA BARRA BOTTONI E AREA TABULATORI
        # Parametri altezza aree fisse (modificabili a piacere)
        self.H_BAR = 40     # altezza barra bottoni (px)
        self.H_TABS = 200   # altezza area tab (px)

        self.TAB_ICON_H = 35      # altezza icone nei TAB (px)  35 metti 32/36 se le vuoi ancora più piccole
        self.TAB_ICON_W_MAX = 120   # larghezza max (px)          ← 72/80 se vuoi stringerle
        self.TAB_ICON_SP = 6       # spaziatura orizz/vert (px)
        self.TAB_ICON_SP_BY_TAB = {"SEGNALI": 2}  # spaziatura più stretta sul tab Segnali

        # Evidenziazione tab attivo: "white" oppure "yellow"
        self.TAB_HILITE = "yellow"  # cambia in "yellow" se preferisci giallo trasparente

        #                                                                                                       TESTATA AREA ROADBOOK
        # --- Contenitore della testata del roadbook---
        top_rb = QFrame()
        top_rb.setObjectName("TopRB")
        top_rb.setStyleSheet("QFrame#TopRB { border: 1px solid black; background: #ffffff; }")
        top_layout = QHBoxLayout()  # NON attaccato a top_rb
        top_rb.setFrameShape(QFrame.NoFrame)
        top_rb.setStyleSheet("background: transparent; border: none;")

        # SINISTRA (1/3): km_totali (su) + note_totali (giù)

        # font configurabili per km totali e note
        self.KM_FONT_PT = 28
        self.NOTE_FONT_PT = 28
        self.LABEL_FONT_PT = 11

        self.km_totali = 0.0
        self.note_totali = 0

        # wrapper superiore: sinistra | sep | destra
        upper = QWidget()
        self.upper_header = upper
        upper_layout = QHBoxLayout(upper)
        upper_layout.setContentsMargins(0, 0, 0, 0)
        upper_layout.setSpacing(0)

        # --- sinistra (1/3) ---
        left_info = QWidget()
        left_layout = QVBoxLayout(left_info)
        left_layout.setContentsMargins(0, 0, 0, 0)
        left_layout.setSpacing(0)

        # Font anti-alias e vettoriale
        font_lbl = QFont("Segoe UI", self.LABEL_FONT_PT)
        font_lbl.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
        font_km = QFont("Segoe UI", self.KM_FONT_PT)
        font_km.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
        font_note = QFont("Segoe UI", self.NOTE_FONT_PT)
        font_note.setHintingPreference(QFont.HintingPreference.PreferNoHinting)

        # Etichetta "Km Totali:"
        lbl_km_title = QLabel("Km Totali:")
        lbl_km_title.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        lbl_km_title.setFont(font_lbl)

        # Valore km
        self.lbl_km = QLabel(f"{self.km_totali:.2f}")
        self.lbl_km.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        self.lbl_km.setFont(font_km)

        # Etichetta "Note:"
        lbl_note_title = QLabel("Note:")
        lbl_note_title.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
        lbl_note_title.setFont(font_lbl)

        # Valore note
        self.lbl_note = QLabel(f"{self.note_totali}")
        self.lbl_note.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
        self.lbl_note.setFont(font_note)

        # Contenitore espandibile (niente minHeight che schiaccia)
        left_info.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)

        # Montaggio
        left_layout.addWidget(lbl_km_title)
        left_layout.addWidget(self.lbl_km)
        left_layout.addStretch(1)
        left_layout.addWidget(lbl_note_title)   # <- etichetta SOPRA
        left_layout.addWidget(self.lbl_note)

        # --- separatore verticale ---
        vsep = QFrame()
        vsep.setFixedWidth(2)  # ~1.5–2 px
        vsep.setStyleSheet("background: black;")

        # --- destra (2/3): logo cliccabile ---
        right_box = QWidget()
        right_box_layout = QVBoxLayout(right_box)
        right_box_layout.setContentsMargins(0, 0, 0, 0)
        right_box_layout.setSpacing(0)
        right_box.setStyleSheet("background:#ffffff;")

        # Logo (clic per cambiare) - scala in base all'altezza, mantieni aspect ratio
        self.default_logo_path = r"C:\Users\usuario\Documents\Invenzioni\RaodbookEditor\RoadBookEditor.png"
        self.logo_path = self.default_logo_path
        self.logo = QLabel()
        self.logo.setAlignment(Qt.AlignCenter)
        self.logo.setStyleSheet("background:#ffffff; border:0; margin:0; padding:0;")
        self.logo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        self.logo.setScaledContents(False)  # NO stretch: ridimensioniamo noi

        # carica pixmap originale
        self.logo_pix = QPixmap(self.logo_path)
        if not self.logo_pix.isNull():
            # primo fit alla altezza attuale del contenitore
            self.logo.setPixmap(self.logo_pix.scaledToHeight(max(1, self.logo.height()), Qt.SmoothTransformation))

        def _fit_logo():
            if hasattr(self, "logo_pix") and not self.logo_pix.isNull():
                h = max(1, self.logo.height())
                self.logo.setPixmap(self.logo_pix.scaledToHeight(h, Qt.SmoothTransformation))

        def _set_logo_from_path(path_logo):
            if not isinstance(path_logo, str) or not path_logo.strip():
                return False
            p = QPixmap(path_logo)
            if p.isNull():
                return False
            self.logo_path = path_logo
            self.logo_pix = p
            _fit_logo()
            return True

        def _set_default_logo():
            _set_logo_from_path(self.default_logo_path)

        def cambia_logo():
            start_dir = os.path.dirname(self.logo_path) if isinstance(self.logo_path, str) and self.logo_path else os.path.dirname(self.default_logo_path)
            fn, _ = QFileDialog.getOpenFileName(
                self, "Scegli logo", start_dir,
                "Immagini (*.png *.jpg *.jpeg)"
            )
            if fn:
                _set_logo_from_path(fn)

        self.logo.mousePressEvent = lambda e: cambia_logo()

        # aggiorna automaticamente quando il contenitore cambia dimensione
        self.logo.installEventFilter(self)
        right_box_layout.addWidget(self.logo, 1)

        # aggiungi i due pannelli all'upper
        upper_layout.addWidget(left_info, 1)
        upper_layout.addWidget(vsep)
        upper_layout.addWidget(right_box, 2)

        # --- titolo sotto (larghezza piena), separato ~5px, cornice 1px ---
        title_frame = QFrame()
        title_frame.setObjectName("TitleFrame")
        title_frame.setStyleSheet("QFrame#TitleFrame { border:1px solid #000; background:#ffffff; }")
        title_layout = QVBoxLayout(title_frame)
        title_layout.setContentsMargins(6, 3, 6, 3)
        title_layout.setSpacing(0)
        title_frame.setFixedHeight(int(self.height() * 0.05))  # 5% dell’altezza finestra

        from PySide6.QtWidgets import QLineEdit
        self.title_edit = QLineEdit()
        self.title_edit.setObjectName("titolo_roadbook")
        self.title_edit.setAlignment(Qt.AlignCenter)
        self.title_edit.setPlaceholderText(self.t("ui.title_placeholder"))
        self.title_edit.setStyleSheet("QLineEdit { border:0; background:transparent; font-weight:bold; font-size:18pt; }")
        self.title_edit.setFixedHeight(45)

        # alias per compatibilità col codice esistente
        self.titolo_roadbook = self.title_edit

        title_layout.addWidget(self.title_edit)

        # ricostruisci il layout di top_rb
        top_v = QVBoxLayout(top_rb)
        top_v.setContentsMargins(4, 4, 4, 4)
        top_v.setSpacing(5)
        top_v.addWidget(upper)
        top_v.addWidget(title_frame)

        top_rb.setFixedHeight(int(self.height() * 0.26))
        self.top_rb = top_rb
        layout_right.addWidget(top_rb)

        # ---                                                                                      Area SCROLL (contenuti/icone/notes) ---       
        self.scroll_area = QScrollArea()
        self.scroll_area.setObjectName("ScrollRB")
        self.scroll_area.setWidgetResizable(True)
        QScroller.grabGesture(self.scroll_area.viewport(), QScroller.LeftMouseButtonGesture)
        self.scroll_area.setFrameShape(QFrame.NoFrame)

        scroll_content = QWidget()
        self.scroll_area.setWidget(scroll_content)
        self.scroll_layout = QVBoxLayout(scroll_content)
        self.scroll_layout.setContentsMargins(6, 0, 6, 0)
        self.scroll_layout.setSpacing(0)

        #                                                                                                LAYOUT CHE CONTIENE LE NOTE
        # --- NOTE SCROLL: una riga (NoteRow) per ogni waypoint ----------------------
        self.note_scroll = QScrollArea()
        self.note_scroll.setWidgetResizable(True)
        self.note_scroll.setFrameShape(QFrame.NoFrame)

        self.note_root = QWidget()
        self.note_v = QVBoxLayout(self.note_root)
        self.note_v.setContentsMargins(0, 0, 0, 0)
        self.note_v.setSpacing(0)
        self.note_scroll.setWidget(self.note_root)

        # Mappe di supporto (nota -> widget; nota -> indice trackpoint)
        self._note_widgets_by_num = {}
        self._note_idx_by_num = {}

        # Inserisci la scroll area delle note PRIMA del blocco fisso in basso
        layout_right.addWidget(self.note_scroll, 1)
        
        self.note_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)   # (oppure AlwaysOn per test)
        self.note_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # ---                                                                                Area FISSA in basso: 2 righe (Barra bottoni + Tabs) -
        # TUTTO: Bottoni + strada:
        # 1) _get_selected_index_from_map(i)
        # 2) aggiorna road_type_in/out su i (e su next se serve)
        # 3) invia_traccia_a_mappa(); QTimer.singleShot(0, self._refresh_notes)

        bottom_fixed = QFrame()
        bottom_fixed.setObjectName("BottomFixed")
        # --- stile barra+tab ---
        sel_bg = "#ffffff" if self.TAB_HILITE == "white" else "rgba(255, 230, 120, 0.35)"
        bottom_fixed.setStyleSheet(f"""
        /* contenitore basso */
        QFrame#BottomFixed {{
            background: #f3f3f3;
            border-top: 1px solid #bcbcbc;
        }}
        
        /* barra bottoni */
        QFrame#ButtonsBar {{
            background: #f3f3f3;
            border-top: 1px solid #bcbcbc;
        }}

        /* area tab */
        QTabWidget::pane {{
            background: #f3f3f3;
            border-top: 1px solid #bcbcbc;
            border-left: none;
            border-right: none;
            border-bottom: none;
            top: -1px;  /* aggancia i tab alla pane */
        }}

        /* tab: arrotondati sopra, grigio chiaro quando non selezionati */
        QTabBar::tab {{
            background: #e9e9e9;
            border: 1px solid #bcbcbc;
            border-bottom: none;
            border-top-left-radius: 8px;
            border-top-right-radius: 8px;
            padding: 6px 12px;
            margin-right: 6px;
        }}

        /* tab selezionato: colore scelto + testo in grassetto */
        QTabBar::tab:selected {{
            background: {sel_bg};
            font-weight: 600;
        }}

        /* piccolo effetto hover sui non selezionati */
        QTabBar::tab:!selected:hover {{
            background: #f2f2f2;
        }}
        """)
        bottom_v = QVBoxLayout(bottom_fixed)
        bottom_v.setContentsMargins(0, 0, 0, 0)
        bottom_v.setSpacing(0)

        # 1) Barra bottoni — altezza fissa parametrica
        bar_frame = QFrame()
        bar_frame.setObjectName("ButtonsBar")
        bar_frame.setStyleSheet("background:#f3f3f3;")
        bar_frame.setFixedHeight(self.H_BAR)
        bar_h = QHBoxLayout(bar_frame)
        bar_h.setContentsMargins(6, 0, 6, 0)
        bar_h.setSpacing(6)

        self._make_toolbar_from_config(bar_h)
        self._apply_tooltips()
        bar_h.addStretch(1)

        # 2) Area tab — 4 tab, altezza fissa parametrica
        self.tabs = QTabWidget()
        self.tabs.setFixedHeight(self.H_TABS)

        #                                                                   Caricamento "lazy": il contenuto del tab viene creato alla prima apertura.
        # 5 tab: TERRENO, AVVISI, INFO!, POI, SEGNALI (localizzati)
        self._tab_map = {}          # prefix -> (scroll_widget, flow_layout, prefix)

        prefixes = ["t", "a", "i", "p", "s"]
        tabs_def = [(self.t(TAB_KEYS[p]), p) for p in prefixes]

        self._tabs_idx = {}
        self._idx_to_tab_prefix = {}

        for nome, prefix in tabs_def:
            sa = QScrollArea()
            sa.setWidgetResizable(True)
            sa.setFrameShape(QFrame.NoFrame)

            content = QWidget()
            sp = self.TAB_ICON_SP_BY_TAB.get(prefix, self.TAB_ICON_SP)  # usa il prefisso (stabile)
            flow = FlowLayout(hspacing=sp, vspacing=sp, margins=(6, 6, 6, 6))
            content.setLayout(flow)
            sa.setWidget(content)

            idx = self.tabs.addTab(sa, nome)
            self._tabs_idx[prefix] = idx
            self._idx_to_tab_prefix[idx] = prefix
            self._tab_map[prefix] = (content, flow, prefix)
            self._init_tab_shortcuts(prefix, content)

        # Stato per gestione icone/shortcut dei TAB
        self._tabs_loaded = set()    # contiene prefissi ("t","a","i","p","s")
        self._tab_selected = {}      # se usi chiavi per tab, meglio prefissi
        self._tab_btn_info = {}

        # Primo tab subito, gli altri on-demand (usa prefisso)
        self._load_tab_icons("t")

        def _on_tab_changed(idx):
            key = self._idx_to_tab_prefix.get(idx)
            if key:
                self._load_tab_icons(key)

        self.tabs.currentChanged.connect(_on_tab_changed)

        bottom_v.addWidget(bar_frame)
        bottom_v.addWidget(self.tabs)

        # l'area fissa in basso ha altezza determinata dalla somma delle due righe
        bottom_fixed.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        layout_right.addWidget(bottom_fixed, 0)

        # Costruzione iniziale delle note (oggi vuote; Passo 2 le popolerà dal database)
        self.rebuild_notes_from_db()
        print("NOTE:", [(m['num'], m['idx'], round(m['km_parz'],2)) for m in self._compute_waypoint_metrics()])
        # --- watcher selezione mappa -> evidenzia/scroll nota associata ---
        self._sel_watch = QTimer(self)
        self._sel_watch.setInterval(250)  # ms
        self._sel_watch.timeout.connect(self.highlight_note_by_selected_point)
        self._sel_watch.start()

    def _menu_mappa(self, pos):
        self._last_ctx_pos = pos
        menu = QMenu(self)
        a_earth = menu.addAction(self.t("map.open_earth"))
        a_wayback = menu.addAction(self.t("map.open_wayback"))
        a_gmaps = menu.addAction(self.t("map.open_gmaps"))
        a_copy = menu.addAction(self.t("map.copy_coords"))

        act = menu.exec(self.web.mapToGlobal(pos))
        if act == a_earth:
            self._open_external("earth")
        elif act == a_wayback:
            self._open_external("wayback")
        elif act == a_gmaps:
            self._open_external("gmaps")
        elif act == a_copy:
            self._open_external("copy")

    def _open_external(self, what):
        pos = getattr(self, "_last_ctx_pos", None)

        if pos is not None:
            x, y = int(pos.x()), int(pos.y())
            js = (
                f"(function(){{try{{"
                f"return window.readLngLatAt?window.readLngLatAt({x},{y}):"
                f"(window.readCursorLngLat?window.readCursorLngLat():null);"
                f"}}catch(e){{return null;}}}})()"
            )
        else:
            js = "(function(){try{return window.readCursorLngLat?window.readCursorLngLat():null;}catch(e){return null;}})()"

        self.web.page().runJavaScript(js, 0, lambda res: self._on_ext_coords(res, what))

    def _on_ext_coords(self, res, what):
        if not res:
            return
        try:
            lng, lat = json.loads(res)  # [lng, lat]
        except Exception:
            return
        if what == "wayback":
            # Wayback accetta mapCenter=lng,lat,zoom nell’hash
            url = f"https://livingatlas.arcgis.com/wayback/#mapCenter={lng}%2C{lat}%2C17"
            webbrowser.open(url)
            return

        if what == "earth":
            # apre Google Earth Pro tramite un KML temporaneo (serve Earth Pro installato)
            kml = f'''<?xml version="1.0" encoding="UTF-8"?>
    <kml xmlns="http://www.opengis.net/kml/2.2"><Document>
    <name>RB Cursor</name>
    <LookAt><longitude>{lng}</longitude><latitude>{lat}</latitude><range>2000</range><tilt>0</tilt><heading>0</heading></LookAt>
    <Placemark><name>Cursor</name><Point><coordinates>{lng},{lat},0</coordinates></Point></Placemark>
    </Document></kml>'''
            fd, path = tempfile.mkstemp(suffix=".kml", prefix="rb_cursor_")
            os.close(fd)
            with open(path, "w", encoding="utf-8") as f:
                f.write(kml)
            os.startfile(path)  # usa l’app associata ai .kml (idealmente Google Earth Pro)
            return

        if what == "gmaps":
            # apri Google Maps centrato sulle coordinate (lat,lng)
            try:
                url = f"https://www.google.com/maps/search/?api=1&query={lat},{lng}"
                webbrowser.open(url)
            except Exception:
                pass
            return

        if what == "copy":
            try:
                cb = QGuiApplication.clipboard()
                s = f"{lat:.6f},{lng:.6f}"
                cb.setText(s)
                try:
                    self.statusBar().showMessage(f"Coordinate copiate: {s}", 3000)
                except Exception:
                    pass
            except Exception:
                pass
            return

    # --- i18n core ---------------------------------------------------------------INTERNAZIONALIZZAZIONE

    from PySide6.QtWidgets import QToolButton

    def _apply_tooltips(self):
        btns = getattr(self, "bar_buttons", {})
        for key, btn in btns.items():
            ik = TOOLTIP_KEYS.get(key)
            tip = self.t(ik) if ik else btn.toolTip()  # fallback: quello già impostato
            # forza refresh su alcune piattaforme
            btn.setToolTip("")
            btn.setToolTip(tip)

    def _init_i18n(self):
        from PySide6.QtCore import QSettings, QLocale
        st = QSettings("Max", "RoadBookEditor")
        lang = st.value("lang", "", type=str)
        if not lang:
            sys_lang = QLocale.system().name().lower()
            lang = "it" if sys_lang.startswith(("it",)) else ("es" if sys_lang.startswith(("es",)) else "en")
        self.current_lang = lang if lang in I18N else "en"

    def t(self, key: str, **kw) -> str:
        s = I18N.get(getattr(self, "current_lang", "en"), {}).get(key,
            I18N.get("en", {}).get(key, key))
        try:
            return s.format(**kw) if kw else s
        except Exception:
            return s

    def set_language(self, lang: str):
        if lang not in I18N:
            return
        self.current_lang = lang
        # persist
        from PySide6.QtCore import QSettings
        QSettings("Max", "RoadBookEditor").setValue("lang", lang)
        self.retranslate_ui()

    def retranslate_ui(self):
        # aggiorna SOLO ciò che sappiamo esistere ora (menu stampa + azione crea pdf + menu Lingue)
        if getattr(self, "menu_print", None):self.menu_print.setTitle(self.t("menu.print"))
        if getattr(self, "menu_file", None): self.menu_file.setTitle(self.t("menu.file"))
        if getattr(self, "act_apri_roadbook", None): self.act_apri_roadbook.setText(self.t("menu.open"))
        if getattr(self, "act_salva_roadbook", None): self.act_salva_roadbook.setText(self.t("menu.save_rb"))
        if getattr(self, "act_esporta_digitale", None): self.act_esporta_digitale.setText(self.t("menu.export_digital"))
        if getattr(self, "act_salva_gpx", None): self.act_salva_gpx.setText(self.t("menu.save_track_gpx"))
        if getattr(self, "menu_track", None): self.menu_track.setTitle(self.t("menu.track"))
        if getattr(self, "act_load_gpx", None): self.act_load_gpx.setText(self.t("action.load_gpx"))
        if getattr(self, "act_riduci_punti_rdp", None): self.act_riduci_punti_rdp.setText(self.t("action.reduce_track_points"))
        if getattr(self, "act_inverti_gpx", None): self.act_inverti_gpx.setText(self.t("action.invert_track"))
        if getattr(self, "act_crea_pdf", None): self.act_crea_pdf.setText(self.t("action.create_pdf"))
        if getattr(self, "menu_languages", None):self.menu_languages.setTitle(self.t("menu.languages"))
        if getattr(self, "act_lang_it", None): self.act_lang_it.setText(self.t("lang.it"))
        if getattr(self, "act_lang_es", None): self.act_lang_es.setText(self.t("lang.es"))
        if getattr(self, "act_lang_en", None): self.act_lang_en.setText(self.t("lang.en"))
        if getattr(self, "menu_map", None): self.menu_map.setTitle(self.t("menu.map"))
        if getattr(self, "act_toggle_etichetta", None): self.act_toggle_etichetta.setText(self.t("action.labels"))
        if getattr(self, "act_map_outdoor", None): self.act_map_outdoor.setText(self.t("action.map_outdoor"))
        if getattr(self, "act_exit", None): self.act_exit.setText(self.t("menu.exit"))
        if getattr(self, "menu_help", None): self.menu_help.setTitle(self.t("menu.help"))
        if getattr(self, "act_help_it", None): self.act_help_it.setText("Apri guida (Italiano)")
        if getattr(self, "act_help_es", None): self.act_help_es.setText("Abrir guía (Español)")
        if getattr(self, "act_help_en", None): self.act_help_en.setText("Open Help (English)")
        if getattr(self, "menu_edit_points", None): self.menu_edit_points.setTitle(self.t("menu.edit_points"))
        if getattr(self, "title_edit", None): self.title_edit.setPlaceholderText(self.t("ui.title_placeholder"))
        if getattr(self, "act_aggiungi_waypoint", None): self.act_aggiungi_waypoint.setText(self.t("action.add_wp"))
        if getattr(self, "act_muovi_punto", None): self.act_muovi_punto.setText(self.t("action.move_pt"))
        if getattr(self, "act_aggiungi_intermedio", None): self.act_aggiungi_intermedio.setText(self.t("action.add_mid_pt"))
        if getattr(self, "act_aggiungi_su_linea", None): self.act_aggiungi_su_linea.setText(self.t("action.add_on_line"))
        if getattr(self, "act_wp_to_tp", None): self.act_wp_to_tp.setText(self.t("action.wp_to_tp"))
        if getattr(self, "act_cancella_punto", None): self.act_cancella_punto.setText(self.t("action.del_pt"))
        self._apply_tooltips()
        if getattr(self, "_tabs_idx", None):
            for p, idx in self._tabs_idx.items():
                self.tabs.setTabText(idx, self.t(TAB_KEYS[p]))
        # traduzioni menu controlla aggiornamenti
        if getattr(self, "act_updates", None):
            self.act_updates.setText({
                "it": "Controlla aggiornamenti",
                "es": "Buscar actualizaciones",
                "en": "Check for updates",
            }.get(getattr(self, "current_lang", "en"), "Check for updates"))

    # si incarica di gestire le dimensioni del logo caricato, ridimensionandolo quando necessario
    def eventFilter(self, obj, event):
        if obj is getattr(self, "logo", None) and event.type() == QEvent.Resize:
            if hasattr(self, "logo_pix") and not self.logo_pix.isNull():
                h = max(1, self.logo.height())
                self.logo.setPixmap(self.logo_pix.scaledToHeight(h, Qt.SmoothTransformation))
            return super().eventFilter(obj, event)
        # MENU contestuale mappa: click destro (senza trascinare) -> apri menu
        if event.type() in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease):
            try:
                if hasattr(event, "button") and event.button() == Qt.RightButton:
                    w = obj
                    while w is not None:
                        if w is getattr(self, "web", None):
                            if event.type() == QEvent.MouseButtonPress:
                                self._map_rc_press = event.globalPosition().toPoint()
                            else:
                                p0 = getattr(self, "_map_rc_press", None)
                                gp = event.globalPosition().toPoint()
                                self._map_rc_press = None
                                if p0 is not None:
                                    dx = gp.x() - p0.x()
                                    dy = gp.y() - p0.y()
                                    if dx*dx + dy*dy <= 16:  # ~4px di tolleranza
                                        self._menu_mappa(self.web.mapFromGlobal(gp))
                                        return True
                            return super().eventFilter(obj, event)
                        w = w.parent()
            except Exception:
                pass

        # NUOVO: clic "fuori" dai vettori → deseleziona
        if event.type() == QEvent.MouseButtonPress:
            try:
                # 1) se clic su un QToolButton (o un suo figlio) -> NON deselezionare
                w = obj
                while w is not None:
                    if isinstance(w, QToolButton):
                        return super().eventFilter(obj, event)
                    w = w.parent()

                # 2) se clic nell'area destra ma NON sul VectorCanvas -> deseleziona
                if hasattr(self, "right") and self.right.isAncestorOf(obj):
                    if getattr(obj, "objectName", "") != "VectorCanvas":
                        self.clear_vector_selection()
                        self.clear_bivio_selection()
            except Exception:
                pass

        return super().eventFilter(obj, event)   

    #                                                                                                seleziona bivio con un trattino rosso

    def clear_bivio_selection(self):
        self._selected_bivio = None
        for row in self._note_widgets_by_num.values():
            if hasattr(row, "canvasB"):
                row.canvasB.set_bivio_selected(False)

    def clear_bivio_selection(self):
        n = getattr(self, "_selected_bivio", None)
        if n is None: return
        row = self._note_widgets_by_num.get(n)
        if row and hasattr(row, "canvasB"):
            row.canvasB.set_bivio_selected(False)
        self._selected_bivio = None

    def on_blank_canvas_click(self):
        self.clear_bivio_selection()
        self.clear_vector_selection()
        self.statusBar().showMessage(f"nota={self._current_note_num}  out={self._selected_vector_num}  bivio={self._selected_bivio}")

    def on_click_bivio(self, num):
        # 1) deseleziona l'eventuale bivio selezionato in precedenza
        prev = getattr(self, "_selected_bivio", None)
        if prev is not None:
            row_prev = self._note_widgets_by_num.get(prev)
            if row_prev and hasattr(row_prev, "canvasB"):
                row_prev.canvasB.set_bivio_selected(False)
                row_prev.canvasB.update()

        # 2) seleziona il bivio della nota 'num'
        self._selected_bivio = num
        row = self._note_widgets_by_num.get(num)
        if row and hasattr(row, "canvasB"):
            row.canvasB.set_bivio_selected(True)
            row.canvasB.update()

        # 3) (opzionale) messaggio di stato
        try:
            self.statusBar().showMessage(f"Bivio selezionato su nota {num}", 1200)
        except Exception:
            pass

        #                                                                                                seleziona vettore freccia con un trattino rosso

    def on_click_vector_out(self, num):
        # deseleziona precedente
        if self._selected_vector_num is not None:
            prev = self._note_widgets_by_num.get(self._selected_vector_num)
            if prev and hasattr(prev, "canvasB"):
                prev.canvasB.set_selected(False)
        # seleziona questo
        self._selected_vector_num = num
        row = self._note_widgets_by_num.get(num)
        if row and hasattr(row, "canvasB"):
            row.canvasB.set_selected(True)
        self.statusBar().showMessage(
            f"OUT su nota={num}  (nota_cur={self._current_note_num or '-'}  bivio_su_nota={self._selected_bivio or '-'})"
        )

    def on_click_vector_out(self, num):
        # deseleziona OUT precedente
        if self._selected_vector_num is not None:
            prev = self._note_widgets_by_num.get(self._selected_vector_num)
            if prev and hasattr(prev, "canvasB"):
                prev.canvasB.set_selected(False)

        # 🔹 azzera qualunque bivio selezionato
        for _n, _row in self._note_widgets_by_num.items():
            if hasattr(_row, "canvasB"):
                _row.canvasB.set_bivio_selected(False)
        self._selected_bivio = None  # se presente

        # seleziona questo OUT
        self._selected_vector_num = num
        row = self._note_widgets_by_num.get(num)
        if row and hasattr(row, "canvasB"):
            row.canvasB.set_selected(True)

        # (facoltativo) msg debug
        self.statusBar().showMessage(
            f"OUT su nota={num}  (bivio deselezionato)"
        )

    def clear_vector_selection(self):
        if self._selected_vector_num is None:
            return
        prev = self._note_widgets_by_num.get(self._selected_vector_num)
        if prev and hasattr(prev, "canvasB"):
            prev.canvasB.set_selected(False)
        self._selected_vector_num = None

    def apply_road_type_out(self, style_dict):
        """Applica il tipo strada al vettore OUT della nota selezionata
        e eredita sull'IN della nota successiva. Aggiorna solo le 2 righe."""
        if not style_dict or self._selected_vector_num is None:
            return

        idx = self._note_idx_by_num.get(self._selected_vector_num)
        if idx is None or idx < 0 or idx >= len(self.database_traccia):
            return

        # scrivi nel DB
        self.database_traccia[idx]["road_type_out"] = style_dict["type"]
        if idx + 1 < len(self.database_traccia):
            self.database_traccia[idx+1]["road_type_in"] = style_dict["type"]

        # helper per tradurre int->dict con fallback a chosen/default
        chosen = getattr(self, 'current_pen_style', None) or road_types[0]
        def _rt(i):
            try:
                i = int(i)
                return road_types[i] if 0 <= i < len(road_types) else chosen
            except Exception:
                return chosen

        # aggiorna riga corrente
        row = self._note_widgets_by_num.get(self._selected_vector_num)
        if row and hasattr(row, "canvasB"):
            b_in, b_out = self._bearing_in_out(idx)
            p = self.database_traccia[idx]
            style_in  = _rt(p.get("road_type_in",  None))
            style_out = _rt(p.get("road_type_out", None))
            row.canvasB.set_bearings(b_in, b_out, style_in, style_out)

        # aggiorna riga successiva (per l'IN ereditato)
        if idx + 1 < len(self.database_traccia):
            next_num = None
            # risali numero nota -> dal mapping già pronto
            for num, i in self._note_idx_by_num.items():
                if i == idx + 1:
                    next_num = num; break
            if next_num is not None:
                row2 = self._note_widgets_by_num.get(next_num)
                if row2 and hasattr(row2, "canvasB"):
                    b_in2, b_out2 = self._bearing_in_out(idx + 1)
                    p2 = self.database_traccia[idx + 1]
                    style_in2  = _rt(p2.get("road_type_in",  None))
                    style_out2 = _rt(p2.get("road_type_out", None))
                    row2.canvasB.set_bearings(b_in2, b_out2, style_in2, style_out2)

        # propaga IN ai waypoint successivi e aggiorna UI
        self._propagate_in_from(idx)
        self._refresh_rows_from_idx(idx+1)  # così non ridisegni due volte la riga corrente
        self.file_modificato_flag = True


    
    #                                                                                                         AREA TAB
    
    #                                                                                              TAB SHORTCUT DENTRO AI TABULATORI
    
    def _init_tab_shortcuts(self, nome_tab, content_widget):
        """Scorciatoie scoping al TAB: Ctrl+V, F2, DEL."""
        QShortcut(QKeySequence.Paste,  content_widget, activated=lambda n=nome_tab: self._on_tab_paste(n))
        QShortcut(QKeySequence(Qt.Key_F2), content_widget, activated=lambda n=nome_tab: self._on_tab_rename(n))
        QShortcut(QKeySequence(Qt.CTRL | Qt.Key_Delete), content_widget, activated=lambda n=nome_tab: self._on_tab_delete(n))

    #                                                                                               SELEZIONA UNA ICONA CON BORDO GIALLO

    def _select_tab_icon(self, nome_tab, btn):
        """Seleziona una tile icona nel TAB (bordo evidenziato)."""
        cur = self._tab_selected.get(nome_tab)
        if cur is btn:  # toggle off?
            return
        # pulisci precedente
        if cur is not None:
            cur.setStyleSheet("QToolButton { border: 0; padding: 0 2px; }")
        # evidenzia nuovo
        btn.setStyleSheet("QToolButton { border: 2px solid #f2c94c; border-radius: 4px; padding: 0 0px; }")
        self._tab_selected[nome_tab] = btn

    
    #                                                                                      INCOLLA IL CONTENUTO DELLA CLIPBOARD NELL'AREA ICONE
    def _on_tab_paste(self, nome_tab):
        """Ctrl+V nel TAB: prende immagine dal clipboard (o file copiato) e crea PNG in ++++++/ con prefisso/numero."""
        _, flow, prefix = self._tab_map[self._idx_to_tab_prefix[self.tabs.currentIndex()]]
        ensure_user_icons()
        icons_dir = str(USER_ICONS)


        cb = QGuiApplication.clipboard()
        md = cb.mimeData()

        img = None
        # 1) Immagine nativa
        try:
            if hasattr(md, "hasImage") and md.hasImage():
                qimg = md.imageData()
                if isinstance(qimg, QImage) and not qimg.isNull():
                    img = qimg
                else:
                    pm_clip = cb.pixmap()
                    if not pm_clip.isNull():
                        img = pm_clip.toImage()
        except Exception:
            pass

        # 2) URI-list (file copiato da Explorer/Finder)
        if img is None and md.hasUrls():
            for u in md.urls():
                if u.isLocalFile():
                    p = u.toLocalFile()
                    pm = QPixmap(p)
                    if not pm.isNull():
                        img = pm.toImage()
                        break

        if img is None or img.isNull():
            QMessageBox.information(self, "Incolla icona", "Il clipboard non contiene un’immagine valida.", QMessageBox.Ok)
            return
        
        #                                                                                                    RIDIMENSIONA LE ICONE COPIATE

        # Resize a H=TAB_ICON_H, W<=TAB_ICON_W_MAX
        H = self.TAB_ICON_H
        WMAX = self.TAB_ICON_W_MAX
        pm = QPixmap.fromImage(img)
        if pm.isNull():
            QMessageBox.information(self, "Incolla icona", "Immagine non valida.", QMessageBox.Ok)
            return
        w_nat, h_nat = pm.width(), pm.height()
        scale = H / max(1, h_nat)
        w_scaled = int(round(w_nat * scale))
        w_scaled = max(24, min(WMAX, w_scaled))
        pm_scaled = pm.scaled(w_scaled, H, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)


        #                                                                                         DÀ UN NUOVO NUMERO PROGRESSIVO ALLE ICONE AGGIUNTE
        # Progressivo successivo
        patt = re.compile(rf"^{prefix}(\d+)[-_]?.*\.png$", re.IGNORECASE)
        max_n = 0
        for fn in os.listdir(icons_dir):
            m = patt.match(fn)
            if m:
                try:
                    max_n = max(max_n, int(m.group(1)))
                except Exception:
                    pass
        num = max_n + 1

        #                                                                                  NOME DELLA ICONA AGGIUNTA E TOOLTIP DEFAULT (PUÒ CAMBAIRE CON f2)
        # Nome base (tooltip) iniziale
        base = "icona"
        safe = re.sub(r"[^a-z0-9_]+", "_", base.lower()).strip("_")
        dest_name = f"{prefix}{num:02d}_{safe}.png"
        dest_path = os.path.join(icons_dir, dest_name)
        # evita collisioni
        k = 1
        while os.path.exists(dest_path):
            dest_name = f"{prefix}{num:02d}_{safe}_{k}.png"
            dest_path = os.path.join(icons_dir, dest_name)
            k += 1

        pm_scaled.save(dest_path, "PNG")

        # Crea tile e registra mapping
        btn = QToolButton()
        btn.setObjectName(f"icon_{prefix}{num:02d}_{safe}")
        btn.setAutoRaise(True)
        tip = f"{prefix.upper()}{num:02d} · {safe}"
        btn.setToolTip(tip)
        btn.setIcon(QIcon(pm_scaled))
        btn.setIconSize(QSize(w_scaled, H))
        btn.setFixedSize(w_scaled + 4, H + 4)
        btn.setStyleSheet("QToolButton { border: 0; padding: 0 2px; }")
        btn.clicked.connect(lambda *_, b=btn, n=nome_tab: self._select_tab_icon(n, b))
        btn.clicked.connect(lambda _=False, b=btn, tab=nome_tab: self._on_tab_icon_click(tab, b))

        flow.addWidget(btn)
        self._tab_btn_info.setdefault(nome_tab, {})[btn] = {
            "file": dest_path,
            "name": dest_name,   # <— aggiunto
            "num": num,
            "label": safe
        }
        self._select_tab_icon(nome_tab, btn)  # seleziona appena incollata

    #                                                                                                 CANCELLAZIONE DI UNA ICONA (SOLO ICONE AGGIUNTE DALL'0UTENTE)

    def _on_tab_delete(self, nome_tab):
        """DEL: elimina la tile selezionata nel TAB (senza conferma)."""
        btn = self._tab_selected.get(nome_tab)
        if not btn:
            return
        info = self._tab_btn_info.get(nome_tab, {}).get(btn)
        if not info:
            return
        try:
            if os.path.exists(info["file"]):
                os.remove(info["file"])
        except Exception:
            pass  # niente messaggi
        # rimuovi dalla UI
        btn.setParent(None)
        self._tab_btn_info[nome_tab].pop(btn, None)
        self._tab_selected[nome_tab] = None
        self.statusBar().showMessage(f"{nome_tab}: icona eliminata", 2000)

    #                                                                                                        f2 PER RINOMINARE, IL NOME È IL TOOLTIP
    def _on_tab_rename(self, nome_tab):
        """F2: rinomina 'nome' (tooltip) della tile selezionata e il file su disco."""
        btn = self._tab_selected.get(nome_tab)
        if not btn:
            return
        info = self._tab_btn_info.get(nome_tab, {}).get(btn)
        if not info:
            return

        # nome attuale = dopo progressivo e underscore
        cur = info.get("label", "")
        text, ok = QInputDialog.getText(self, "Rinomina icona", "Nuovo nome:", text=cur)
        if not ok:
            return
        new_label = re.sub(r"[^a-z0-9_]+", "_", text.strip().lower()).strip("_")
        if not new_label:
            new_label = "icona"

        # ricava prefisso/num ed estensione
        old_path = info["file"]
        dirname = os.path.dirname(old_path)
        m = re.search(r"^([taips])(\d+).*?(\.png)$", os.path.basename(old_path), re.IGNORECASE)
        if not m:
            return
        prefix = m.group(1).lower()
        num = int(m.group(2))
        ext = m.group(3).lower()

        new_name = f"{prefix}{num:02d}_{new_label}{ext}"
        new_path = os.path.join(dirname, new_name)
        k = 1
        while os.path.exists(new_path) and new_path.lower() != old_path.lower():
            new_name = f"{prefix}{num:02d}_{new_label}_{k}{ext}"
            new_path = os.path.join(dirname, new_name)
            k += 1

        try:
            os.replace(old_path, new_path)
        except Exception:
            return

        # aggiorna mapping, tooltip
        info["file"] = new_path
        info["name"] = os.path.basename(new_path)
        info["label"] = new_label
        btn.setToolTip(f"{prefix.upper()}{num:02d} · {new_label}")
    
    #                                                                                                      Update menú per disabilitarlo/abilitarlo
    def update_menu_traccia(self):
        punti = len(getattr(self, "database_traccia", []))

        # Inverti traccia: attivo solo con esattamente 2 waypoint (inizio/fine) e ≥ 2 punti
        if getattr(self, "act_inverti_gpx", None):
            solo_due = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None) == 2
            self.act_inverti_gpx.setEnabled(punti >= 2 and solo_due)

        # Riduci: attivo con ≥ 3 punti e solo se il file NON è modificato
        if getattr(self, "act_riduci_punti_rdp", None):
            self.act_riduci_punti_rdp.setEnabled(punti >= 3 and not getattr(self, "file_modificato_flag", False))

        # Aggiungi waypoint: attivo se esiste almeno 1 punto
        if getattr(self, "act_aggiungi_waypoint", None):
            self.act_aggiungi_waypoint.setEnabled(punti >= 1)

        # Muovi punto: attivo se esiste almeno 1 punto
        if getattr(self, "act_muovi_punto", None):
            self.act_muovi_punto.setEnabled(punti >= 1)

        # Aggiungi punto intermedio: attivo se esiste almeno 2 punti
        if getattr(self, "act_aggiungi_intermedio", None):
            self.act_aggiungi_intermedio.setEnabled(punti >= 2)

        # Aggiungi punto su linea: attivo se ci sono almeno 3 punti
        if getattr(self, "act_aggiungi_su_linea", None):
            self.act_aggiungi_su_linea.setEnabled(punti >= 2)

        # Trasforma waypoint in trackpoint: attivo solo se i waypoint sono > 2
        if getattr(self, "act_wp_to_tp", None):
            count_wp = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
            self.act_wp_to_tp.setEnabled(count_wp > 2)

        # Cancella punto: attivo con almeno 3 punti
        if getattr(self, "act_cancella_punto", None):
            self.act_cancella_punto.setEnabled(punti >= 3)

        # Crea PDF: attivo se esiste almeno 1 punto
        if getattr(self, "act_crea_pdf", None):
            self.act_crea_pdf.setEnabled(punti >= 1)
  
    #                                                                                                               DEFINIZIONI FUNZIONI

    # ---                                                                                                       FUNZIONI DI SUPPORTO STAMPA ---

    def _deselect_all_for_print(self):
        # stato interno
        try: self._current_note_num = None
        except: pass

        # termina modalità speciali
        for fn in ("termina_muovi_punto", "clear_vector_selection", "clear_bivio_selection", "clear_note_selection"):
            try: getattr(self, fn)()
            except: pass

        # pulizia tab/icone
        try:
            if hasattr(self, "_tab_selected"):
                for nome, btn in list(self._tab_selected.items()):
                    if btn: btn.setStyleSheet("QToolButton { border: 0; padding: 0 2px; }")
                    self._tab_selected[nome] = None
        except: pass

        # deseleziona tutte le NOTE (rimuove eventuale highlight)
        try:
            for nota in self._iter_note_widgets():
                try:
                    nota.setProperty("selected", False)
                    # forza ripolish per applicare lo stile "non selezionato"
                    nota.style().unpolish(nota); nota.style().polish(nota)
                    nota.update()
                except: 
                    pass
        except: 
            pass
                # rimuovi focus da qualunque widget (evita highlight residui)
        try:
            fw = QApplication.focusWidget()
            if fw:
                fw.clearFocus()
            self.setFocus(Qt.OtherFocusReason)
        except:
            pass

        # mappa (se presente)
        try:
            if hasattr(self, "web") and hasattr(self.web, "page"):
                self.web.page().runJavaScript("try{ if(window.clearSelection) clearSelection(); }catch(e){};", 0)
        except:
            pass

    def _grab_header_image(self, margin=4):
        """
        Cattura l'intestazione (solo riga superiore). Niente titolo qui.
        """
        parent = getattr(self, "right", self)

        upper = getattr(self, "upper_header", None)
        if upper and hasattr(upper, "mapTo") and hasattr(upper, "size"):
            tl = upper.mapTo(parent, QPoint(0, 0))
            rect = QRect(tl, upper.size()).adjusted(-margin, -margin, margin, margin)
            pm = parent.grab(rect)
            return pm.toImage() if not pm.isNull() else None

        # fallback vecchio (puoi lasciarlo)
        names = ("valore_km", "valore_note", "linea_separatore_verticale", "logo_label")
        rect = None
        ...
        for n in names:
            w = getattr(self, n, None)
            if hasattr(w, "mapTo") and hasattr(w, "size"):
                tl = w.mapTo(parent, QPoint(0, 0))
                r = QRect(tl, w.size())
                rect = r if rect is None else rect.united(r)
        if not rect or rect.isEmpty():
            rect = QRect(0, 0, parent.width(), min(220, parent.height()))
        rect = rect.adjusted(-margin, -margin, margin, margin)
        pm = parent.grab(rect)
        return pm.toImage() if not pm.isNull() else None

    def _grab_title_image(self, margin=2):
        """Cattura il titolo esattamente come lo vedi (il QLineEdit)."""
        parent = getattr(self, "right", self)
        w = getattr(self, "titolo_roadbook", None)
        if not (hasattr(w, "mapTo") and hasattr(w, "size")):
            return None
        tl = w.mapTo(parent, QPoint(0, 0))
        rect = QRect(tl, w.size()).adjusted(-margin, -margin, margin, margin)
        pm = parent.grab(rect)
        return pm.toImage() if not pm.isNull() else None

    def _iter_note_widgets(self):
        """
        Restituisce i widget delle note nell'ordine (colonna destra).
        Usa self.layout_roadbook se presente, altrimenti prova il layout di self.note_root.
        """
        lay = getattr(self, "layout_roadbook", None)
        if lay is None and hasattr(self, "note_root") and hasattr(self.note_root, "layout"):
            lay = self.note_root.layout()
        if not lay:
            return []
        out = []
        for i in range(lay.count()):
            it = lay.itemAt(i)
            w = it.widget() if it else None
            if w:
                out.append(w)
        return out
    
    def _open_pdf(self, filename: str):
        base = os.path.dirname(os.path.abspath(__file__))
        path = os.path.join(base, filename)
        if not os.path.exists(path):
            QMessageBox.warning(self, "File non trovato", f"Non trovo '{filename}' nella cartella del programma.")
            return
        QDesktopServices.openUrl(QUrl.fromLocalFile(path))
    
    def crea_pdf(self):
        # 1) pulizia selezioni
        self._deselect_all_for_print()

        # 2) dialogo stampa nativo (scelta orientamento ecc.)
        tmp_printer = QPrinter(QPrinter.HighResolution)
        tmp_printer.setOutputFormat(QPrinter.NativeFormat)
        tmp_printer.setColorMode(QPrinter.Color)
        tmp_printer.setFullPage(True)
        tmp_printer.setResolution(300)

        dlg = QPrintDialog(tmp_printer, self)
        if dlg.exec() != QPrintDialog.Accepted:
            return
        # Avviso: meglio salvare prima (titolo/rotta)
        tit = ""
        try: tit = self.titolo_roadbook.text() or ""
        except: pass
        if "inserire un nome" in (tit.lower() if tit else ""):
            QMessageBox.information(self, self.t("msg.title.info"), self.t("info.save_before_print"))

        # cursore d’attesa + blocco azione per evitare doppi CTRL+P
        try: 
            if hasattr(self, "act_crea_pdf"): self.act_crea_pdf.setEnabled(False)
        except: pass
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.statusBar().showMessage(self.t("status.pdf_working"), 0)

        # 3) dove salvare
        base_name = (getattr(self, 'title_edit', None).text() if hasattr(self, 'title_edit') else "") or (getattr(self, 'nome_file_gpx', '') or 'roadbook')
        pdf_name = f"{base_name}.pdf"
        start_dir = os.path.dirname(getattr(self, "last_roadbook_path", "") or "") or os.getcwd()
        predef = os.path.join(start_dir, pdf_name)

        fn, _ = QFileDialog.getSaveFileName(self, "Salva come PDF", predef, "PDF (*.pdf)")
        if not fn:
            return
        if not fn.lower().endswith(".pdf"):
            fn += ".pdf"

        # 4) QPrinter in PDF + layout copiato
        printer = QPrinter(QPrinter.HighResolution)
        printer.setOutputFormat(QPrinter.PdfFormat)
        printer.setColorMode(QPrinter.Color)
        printer.setFullPage(True)
        try:
            printer.setPageLayout(tmp_printer.pageLayout())
        except Exception:
            printer.setOrientation(tmp_printer.orientation())
            printer.setPageSize(tmp_printer.pageSize())
        printer.setOutputFileName(fn)
        printer.setResolution(300)

        # 5) impaginazione
        # assicura layout calcolati
        for w in (getattr(self, "right", None), getattr(self, "note_root", None)):
            if w and hasattr(w, "layout") and w.layout():
                w.layout().activate()
                w.update()
        QApplication.processEvents()

        painter = QPainter(printer)
        try:
            pw = max(1, printer.width())
            ph = max(1, printer.height())
            m  = int(min(pw, ph) * 0.04)  # margine
            x0 = m
            y  = m
            avail_w = pw - 2*m
            avail_h = ph - 2*m

            # sfondo
            painter.fillRect(0, 0, pw, ph, Qt.white)

            # --- PRIMA PAGINA: header identico + titolo identico ---
            header_img = self._grab_header_image()
            if header_img and not header_img.isNull():
                max_h_header = int(avail_h * 0.18)
                s = min(avail_w / header_img.width(), max_h_header / header_img.height())
                hw = int(header_img.width() * s)
                hh = int(header_img.height() * s)
                painter.drawImage(x0 + (avail_w - hw)//2, y,
                                header_img.scaled(hw, hh, Qt.KeepAspectRatio, Qt.SmoothTransformation))
                y += hh + int(m * 0.25)

            title_img = self._grab_title_image()
            if title_img and not title_img.isNull():
                max_h_title = int(avail_h * 0.07)
                s = min(avail_w / title_img.width(), max_h_title / title_img.height())
                tw = int(title_img.width() * s)
                th = int(title_img.height() * s)
                painter.drawImage(x0 + (avail_w - tw)//2, y,
                                title_img.scaled(tw, th, Qt.KeepAspectRatio, Qt.SmoothTransformation))
                y += th + int(m * 0.25)

            # helper: nuova pagina (dalle successive SOLO note)
            def new_page():
                printer.newPage()
                painter.fillRect(0, 0, pw, ph, Qt.white)
                return m  # riparti solo con margine alto

            # parametri scala note
            dpi = max(150, printer.resolution())
            mm_to_px = lambda mm: int(round(mm * dpi / 25.4))
            row_h_px = mm_to_px(40)  # altezza target di una nota ~ 40 mm (regolabile)
            gap_px   = mm_to_px(3)

            # --- NOTE: impagina a larghezza pagina, n pagine ---
            notes = self._iter_note_widgets()
            for nota in notes:
                # neutralizza highlight giallo SOLO per lo snapshot
                try:
                    _old_style = nota.styleSheet()
                    if hasattr(nota, "set_selected"):
                        nota.set_selected(False)  # toglie il giallo (#fff9c4)
                    else:
                        # fallback: stile "non selezionato"
                        nota.setStyleSheet("#note-row { border-bottom:1px solid #bdbdbd; }")
                    nota.update()
                except Exception:
                    _old_style = None


                snap = nota.grab().toImage()
                if snap.isNull():
                    continue

                # --- griglia nera spessa (compatibilità RpiRoadBook) ---
                try:
                    qp = QPainter(snap)
                    qp.setRenderHint(QPainter.Antialiasing, False)

                    pen = QPen(Qt.black)
                    pen.setWidth(6)  # <-- prova 6; se serve aumenta a 8/10
                    pen.setCapStyle(Qt.SquareCap)
                    qp.setPen(pen)

                    wimg, himg = snap.width(), snap.height()

                    # bordo esterno della "case"
                    qp.drawRect(0, 0, wimg - 1, himg - 1)

                    # separatori verticali tra le 3 colonne
                    if hasattr(nota, "sep1"):
                        x1 = nota.sep1.geometry().center().x()
                        qp.drawLine(x1, 0, x1, himg - 1)

                    if hasattr(nota, "sep2"):
                        x2 = nota.sep2.geometry().center().x()
                        qp.drawLine(x2, 0, x2, himg - 1)

                    qp.end()
                except Exception:
                    pass


                # scala per larghezza
                sW = avail_w / snap.width()
                nw = int(snap.width() * sW)
                nh = int(snap.height() * sW)
                # limita altezza riga
                if nh > row_h_px:
                    sH = row_h_px / nh
                    nw = int(nw * sH)
                    nh = int(nh * sH)

                # se non ci sta, nuova pagina (senza header/titolo)
                if y + nh > m + avail_h:
                    y = new_page()
                img = snap.scaled(nw, nh, Qt.KeepAspectRatio, Qt.SmoothTransformation)

                # niente trasparenza (aiuta compressione / evita stranezze)
                if img.hasAlphaChannel():
                    rgb = QImage(img.size(), QImage.Format_RGB32)
                    rgb.fill(Qt.white)
                    qp2 = QPainter(rgb)
                    qp2.drawImage(0, 0, img)
                    qp2.end()
                    img = rgb

                # JPEG qualità 80
                ba = QByteArray()
                buf = QBuffer(ba)
                buf.open(QIODevice.WriteOnly)
                img.save(buf, "JPG", 80)
                buf.close()
                img = QImage.fromData(ba, "JPG")
                painter.drawImage(x0 + (avail_w - nw)//2, y, img)
                y += nh + gap_px
        finally:
            painter.end()

        # 6) feedback
        # ripristini UI
        QApplication.restoreOverrideCursor()
        self.statusBar().clearMessage()
        try:
            if hasattr(self, "act_crea_pdf"): self.act_crea_pdf.setEnabled(True)
        except:
            pass

        self.statusBar().showMessage(self.t("status.pdf_saved", path=fn), 5000)
        QMessageBox.information(self, "Info", self.t("status.pdf_saved", path=fn))


    #                                                                                                                MENUU MAPPA CTRL+0
    # CAMBIA LA MAPPA etichette/senza etichette
    def toggle_etichetta(self, checked):
        # checked = True → etichette ON
        if checked:
            self.web.page().runJavaScript("window.toggleLabels(true)")
        else:
            self.web.page().runJavaScript("window.toggleLabels(false)")

    def mappa_outdoor(self):
        # Outdoor ha già etichette: metto ON per coerenza UI
        self.act_toggle_etichetta.setChecked(True)
        self.web.page().runJavaScript("window.setOutdoor && window.setOutdoor()")

    #                                                                                                                    ESCI CTRL+Q
    # chiudi il programma
    def close_program(self):
        # chiudi eventuale Muovi punto ancora attivo
        self.termina_muovi_punto()
        if self.file_modificato_flag and not self.warning_chiudiSovrascrivi():
            return
        self.close()  # non QApplication.quit()

    # intercetta la chiusura del pulsante della finestra
    def closeEvent(self, event):
        # chiudi eventuale Muovi punto ancora attivo
        self.termina_muovi_punto()
        if self.file_modificato_flag and not self.warning_chiudiSovrascrivi():
            event.ignore()
        else:
            event.accept()

    #                                                                                                                DEFINIZIONI FILE GPX

    #                                                                                                                 CARICA GPX CTRL+X
    # carica un file.gpx
    def carica_gpx(self):
        self.roadbook_caricato = False
        # chiudi eventuale Muovi punto ancora attivo
        self.termina_muovi_punto()

        # controlliamo prima di aprire se il file in memoria è stato modificato e non salvato.
        if self.file_modificato_flag:  
            if not self.warning_chiudiSovrascrivi():
                return

        # Selezione file
        percorso_file_gpx, _ = QFileDialog.getOpenFileName(self, "Seleziona un file GPX", "", "File GPX (*.gpx)")
        if not percorso_file_gpx:
            return

        # Lettura contenuto file
        with open(percorso_file_gpx, "r", encoding="utf-8") as f:
            contenuto_file_gpx = f.read()

        # Parsing XML semplice per estrarre trkpt
        import xml.etree.ElementTree as ET
        root_gpx = ET.fromstring(contenuto_file_gpx)

        # Namespace se presente
        ns = {}
        if "}" in root_gpx.tag:
            ns['ns'] = root_gpx.tag.split("}")[0].strip("{")

        # Cancella database_traccia precedente
        self.database_traccia = []

        # Estrazione punti dalla traccia
        trackpoints_traccia = root_gpx.findall(".//ns:trkpt", ns) if ns else root_gpx.findall(".//trkpt")
        num_punti_traccia = len(trackpoints_traccia)

        for indice, tp in enumerate(trackpoints_traccia):
            lat_trackpoint = float(tp.attrib["lat"])
            lon_trackpoint = float(tp.attrib["lon"])

            if indice == 0:
                waypoint_nota = 1
            elif indice == num_punti_traccia - 1:
                waypoint_nota = 2
            else:
                waypoint_nota = None
            
            # inizializza a 0 quando crei il punto
            road_type_in = 0
            road_type_out = 0

            self.database_traccia.append({
                "indice": indice,
                "waypoint_nota": waypoint_nota,
                "lat_trackpoint": lat_trackpoint,
                "lon_trackpoint": lon_trackpoint,
                "road_type_in": road_type_in,
                "road_type_out": road_type_out
            })

        # stato iniziale: solo caricamento → non modificato
        self.file_modificato_flag = False
        self.num_punti_traccia = len(self.database_traccia)

        # Aggiorna titolo roadbook con nome file (senza estensione)
        self.nome_file_gpx = os.path.basename(percorso_file_gpx)
        self.nome_file_gpx = os.path.splitext(self.nome_file_gpx)[0]
        self.title_edit.setText(self.nome_file_gpx)

        # --- finale carica_gpx: aggiorna UI, mappa e note ---
        self.aggiorna_km_totali()

        self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
        if hasattr(self, "lbl_note"):
            self.lbl_note.setText(str(self.note_totali))

        self.update_menu_traccia()
        self.invia_traccia_a_mappa()

        # ricentra quando lo stile è pronto (se hai recenterFirst in JS)
        if hasattr(self, "web") and hasattr(self.web, "page"):
            self.web.page().runJavaScript(
                "map && map.once('idle', ()=> window.recenterFirst && window.recenterFirst())", 0
            )

        # note grafiche (sincrono: qui va bene)
        self.rebuild_notes_from_db()
        self.highlight_note_by_selected_point()

        #                                                                                                          MRSSAGGIO E RIDUZIONE PUNTI

        #                                                                                                                 RIDUCI PUNTI
       # Mostra sempre la riduzione di punti
        msg_rdp = QMessageBox(self)
        msg_rdp.setWindowTitle("Riduzione punti")
        msg_rdp.setText(f"Hai caricato {num_punti_traccia} punti, vuoi provare a ridurli?")
        msg_rdp.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        msg_rdp.setDefaultButton(QMessageBox.No)
        risposta_rdp = msg_rdp.exec()

        if risposta_rdp == QMessageBox.Yes:
            # fa tutto lei (riduzione + invio a mappa + rebuild note nella sua funzione)
            self.riduci_punti_rdp(self.soglia_rdp_default)
        else:
            # finale carica_gpx: aggiorna UI, mappa e note (senza riduzione)
            self.aggiorna_km_totali()
            self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
            if hasattr(self, "lbl_note"):
                self.lbl_note.setText(str(self.note_totali))
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()
            if hasattr(self, "web") and hasattr(self.web, "page"):
                self.web.page().runJavaScript(
                    "map && map.once('idle', ()=> window.recenterFirst && window.recenterFirst())", 0
                )
            self.aggiorna_status("Caricata traccia GPX")
            self.rebuild_notes_from_db()
            self.highlight_note_by_selected_point()


    #                                                                                                       FUNZIONE RDP RIDUZIONWE PUNTI CTRL+R
    # funzione RDP per ridurre i punti della traccia
    def riduci_punti_rdp(self, soglia_rdp: float): 
        # chiudi eventuale Muovi punto ancora attivo
        self.termina_muovi_punto()

        if getattr(self, "roadbook_caricato", False):
            self.aggiorna_status("Riduzione punti non consentita su un roadbook.")
            return

        if getattr(self, "file_modificato_flag", False):
            QMessageBox.warning(self, "Operazione non consentita",
                                "Il file è stato modificato. Salva o ricarica prima di ridurre i punti.")
            return
        if len(self.database_traccia) < 3:
            return

        # helper: distanza punto-linea (approssimata su piano in radianti -> metri)
        def dist_punto_linea_m(p, a, b):
            lat, lon  = math.radians(p["lat_trackpoint"]),  math.radians(p["lon_trackpoint"])
            lat1, lon1= math.radians(a["lat_trackpoint"]), math.radians(a["lon_trackpoint"])
            lat2, lon2= math.radians(b["lat_trackpoint"]), math.radians(b["lon_trackpoint"])
            dx = lon2 - lon1
            dy = lat2 - lat1
            if dx == 0 and dy == 0:
                return math.hypot(lat - lat1, lon - lon1) * 6371000.0
            t = ((lon - lon1)*dx + (lat - lat1)*dy) / (dx*dx + dy*dy)
            t = 0.0 if t < 0.0 else (1.0 if t > 1.0 else t)
            proj_lon = lon1 + t*dx
            proj_lat = lat1 + t*dy
            return math.hypot(lat - proj_lat, lon - proj_lon) * 6371000.0

        pts = self.database_traccia
        n0 = len(pts)
        keep = [False]*len(pts)
        keep[0] = keep[-1] = True

        stack = [(0, len(pts)-1)]
        while stack:
            start, end = stack.pop()
            if end <= start+1:
                continue
            # trova punto più distante dal segmento start-end
            max_d = -1.0
            max_i = -1
            a, b = pts[start], pts[end]
            for i in range(start+1, end):
                d = dist_punto_linea_m(pts[i], a, b)
                if d > max_d:
                    max_d = d
                    max_i = i
            if max_d > soglia_rdp:
                keep[max_i] = True
                stack.append((start, max_i))
                stack.append((max_i, end))

        # ricostruzione lista ridotta
        nuova = [p for k, p in zip(keep, pts) if k]
        if len(nuova) == n0:
            self.statusBar().showMessage(f"Nessun punto da ridurre: già ottimizzata: {self.nome_file_gpx} ({self.num_punti_traccia} punti)")
            return

        # re-indicizza e waypoint: 1 al primo, 2 all’ultimo, None gli altri
        for i, p in enumerate(nuova):
            p["indice"] = i
            p["waypoint_nota"] = 1 if i == 0 else (2 if i == len(nuova)-1 else None)

        self.database_traccia = nuova
        self.file_modificato_flag = True
        self.update_menu_traccia()
        self.num_punti_traccia = len(self.database_traccia)
        self.aggiorna_km_totali()
        self.note_totali = sum(1 for p in self.database_traccia if p["waypoint_nota"] is not None)
        self.lbl_note.setText(str(self.note_totali))
        self.invia_traccia_a_mappa()

        # ↓ ricentra quando lo style è pronto
        self.web.page().runJavaScript(
            "map && map.once('idle', ()=> window.recenterFirst && window.recenterFirst())", 0
        )
        # Aggiorna status bar
        self.aggiorna_status(f"Ridotti i punti (RDP={soglia_rdp:.1f} m)")

        # aggiorna UI note
        self.rebuild_notes_from_db()
        self.highlight_note_by_selected_point()

    def aggiorna_km_totali(self):
        def haversine(lat1, lon1, lat2, lon2):
            R = 6371.0
            dlat = math.radians(lat2 - lat1)
            dlon = math.radians(lon2 - lon1)
            a = (math.sin(dlat/2)**2 +
                math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
                math.sin(dlon/2)**2)
            return 2 * R * math.asin(math.sqrt(a))

        km = 0.0
        for i in range(1, len(self.database_traccia)):
            p0 = self.database_traccia[i-1]
            p1 = self.database_traccia[i]
            km += haversine(p0["lat_trackpoint"], p0["lon_trackpoint"],
                            p1["lat_trackpoint"], p1["lon_trackpoint"])

        self.km_totali = km
        self.lbl_km.setText(f"{self.km_totali:.2f}")
        self.update_menu_traccia()
        #self.file_modificato_flag = True

    #                                                                                                          INVERTI LA TRACCIA CTRL+I
    # Inverti una traccia.gpx 
    def inverti_gpx(self):
        # chiudi eventuale Muovi punto ancora attivo
        self.termina_muovi_punto()

        #print("inverti_gpx chiamata:", len(self.database_traccia), "punti")
        # consenti solo se ci sono al minimo i punti necessari
        if len(self.database_traccia) < 2:
            return

        # consenti solo se ci sono esattamente le due note iniziali/finali
        if sum(1 for p in self.database_traccia if p["waypoint_nota"] is not None) != 2:
            QMessageBox.warning(self, "Operazione non consentita",
                                "Puoi invertire solo se esistono le sole due note iniziali/finali.")
            return

        # inverti l'ordine
        self.database_traccia.reverse()

        # re-indicizza e rimetti 1/2 ai capi
        n = len(self.database_traccia)
        for i, p in enumerate(self.database_traccia):
            p["indice"] = i
            p["waypoint_nota"] = 1 if i == 0 else (2 if i == n-1 else None)

        # aggiorna contatori e UI
        self.num_punti_traccia = n
        # self.note_totali = 2
        self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
        if hasattr(self, "lbl_note"):
            self.lbl_note.setText(str(self.note_totali))
        self.aggiorna_km_totali()

        self.file_modificato_flag = True
        self.update_menu_traccia()
        self.statusBar().showMessage(
            f"Invertita la traccia: {self.nome_file_gpx} ({self.num_punti_traccia} punti)", 0
        )
        self.invia_traccia_a_mappa()
        self.web.page().runJavaScript(
            "map && map.once('idle', ()=> window.recenterFirst && window.recenterFirst())", 0
        )
        
        # aggiorna status bar
        self.aggiorna_status("Invertita la traccia")

        # aggiorna UI note
        self.rebuild_notes_from_db()
        self.highlight_note_by_selected_point()


    #                                                                                                           SALVA TRACCIA (SOLO GPX  CTRL+G 
    # Salva una traccia come file.gpx
    def salva_gpx(self):
        # chiudi eventuale Muovi punto ancora attivo
        self.termina_muovi_punto()

        if not getattr(self, "database_traccia", None) or len(self.database_traccia) < 2:
            QMessageBox.warning(self, "Nessuna traccia", "Carica prima una traccia.")
            return

        titolo = (self.title_edit.text().strip() if hasattr(self, "title_edit") else "")
        directory = os.path.dirname(getattr(self, "last_roadbook_path", "") or "") or os.getcwd()
        default_name = os.path.join(directory, f"{self._slug(titolo or self.nome_file_gpx or 'traccia')}.gpx")
        fn, _ = QFileDialog.getSaveFileName(self, "Salva traccia GPX", default_name, "File GPX (*.gpx)")
        if not fn:
            return
        if not fn.lower().endswith(".gpx"):
            fn += ".gpx"

        import xml.etree.ElementTree as ET
        gpx = ET.Element("gpx", attrib={
            "version": "1.1",
            "creator": "Offroad Editor",
            "xmlns": "http://www.topografix.com/GPX/1/1"
        })

        # waypoint delle note
        for p in self.database_traccia:
            num = p.get("waypoint_nota")
            lat = p.get("lat_trackpoint")
            lon = p.get("lon_trackpoint")

            if num is None or lat is None or lon is None:
                continue

            wpt = ET.SubElement(gpx, "wpt", attrib={
                "lat": f"{lat:.6f}",
                "lon": f"{lon:.6f}"
            })
            wpt_name = ET.SubElement(wpt, "name")
            wpt_name.text = str(num)

        trk = ET.SubElement(gpx, "trk")
        name = ET.SubElement(trk, "name")
        name.text = self.nome_file_gpx or "traccia"
        trkseg = ET.SubElement(trk, "trkseg")
        for p in self.database_traccia:
            ET.SubElement(trkseg, "trkpt", attrib={
                "lat": f"{p['lat_trackpoint']:.6f}",
                "lon": f"{p['lon_trackpoint']:.6f}"
            })

        ET.ElementTree(gpx).write(fn, encoding="utf-8", xml_declaration=True)

        # Aggiorna status bar
        self.statusBar().showMessage(f"Salvata la traccia: {os.path.basename(fn)} ({len(self.database_traccia)} punti)")
        self.aggiorna_status("Salvata traccia GPX")

    def _find_prev_wp_idx(self, idx):
        for j in range(idx-1, -1, -1):
            if self.database_traccia[j].get("waypoint_nota") is not None:
                return j
        return None

    def _find_next_wp_idx(self, idx):
        for j in range(idx+1, len(self.database_traccia)):
            if self.database_traccia[j].get("waypoint_nota") is not None:
                return j
        return None
    
    def _update_caps_after_move(self, i_moved: int):
        """Dopo lo spostamento di un punto, aggiorna i CAP rossi coinvolti.
        Filosofia: CAP rosso di un waypoint punta sempre al waypoint successivo.
        """
        if i_moved is None: 
            return
        try:
            i_moved = int(i_moved)
        except Exception:
            return
        if not (0 <= i_moved < len(self.database_traccia)):
            return

        def _recalc_cap_for_wp(idx_wp: int):
            """Ricalcola cap_hdr e cap_km per un waypoint idx_wp (se cap_hdr è attivo)."""
            if idx_wp is None: 
                return
            p = self.database_traccia[idx_wp]
            if p.get("waypoint_nota") is None:
                return
            if p.get("cap_hdr") is None:
                return  # CAP spento: non toccare

            next_wp = self._find_next_wp_idx(idx_wp)
            if next_wp is None:
                # ultimo waypoint: non ha destinazione
                p["cap_hdr"] = None
                p["cap_km"]  = None
                p["hide_cap_btm"] = False
                return

            try:
                brg = float(self._bearing_deg(self.database_traccia[idx_wp], self.database_traccia[next_wp]))

                # distanza km (stesso identico calcolo del bottone CAP)
                R = 6371.0
                a = self.database_traccia[idx_wp]
                b = self.database_traccia[next_wp]
                lat1 = math.radians(float(a["lat_trackpoint"])); lon1 = math.radians(float(a["lon_trackpoint"]))
                lat2 = math.radians(float(b["lat_trackpoint"])); lon2 = math.radians(float(b["lon_trackpoint"]))
                dφ = lat2 - lat1
                dλ = lon2 - lon1
                s = math.sin(dφ/2)**2 + math.cos(lat1)*math.cos(lat2)*math.sin(dλ/2)**2
                km = 2 * R * math.asin(math.sqrt(s))

                p["cap_hdr"] = brg
                p["cap_km"]  = km
                p["hide_cap_btm"] = True
            except Exception:
                # se fallisce non distruggere lo stato, ma almeno evita crash
                return

        # 1) aggiorna il waypoint precedente (se esiste) perché la sua destinazione è i_moved
        prev_wp = self._find_prev_wp_idx(i_moved)
        _recalc_cap_for_wp(prev_wp)

        # 2) aggiorna anche il waypoint stesso (se ha cap attivo) perché è cambiata la sua origine
        _recalc_cap_for_wp(i_moved)

    def _init_road_types_for_new_waypoint(self, idx):
        prev_idx = self._find_prev_wp_idx(idx)
        if prev_idx is not None:
            prev_out = int(self.database_traccia[prev_idx].get("road_type_out", 0) or 0)
            if int(self.database_traccia[idx].get("road_type_in", 0) or 0) == 0:
                self.database_traccia[idx]["road_type_in"] = prev_out
            if int(self.database_traccia[idx].get("road_type_out", 0) or 0) == 0:
                self.database_traccia[idx]["road_type_out"] = int(self.database_traccia[idx]["road_type_in"])

        next_idx = self._find_next_wp_idx(idx)
        if next_idx is not None:
            self.database_traccia[next_idx]["road_type_in"] = int(self.database_traccia[idx].get("road_type_out", 0) or 0)

    #                                                                                                      DEFINIZIONE MENU EDITA PUNTI TRACCIA

    #                                                                                                               AGGIUNGI WAIPOINT "W"
    # Aggiungi Waypoint W
    def aggiungi_waypoint(self):
        self.clear_vector_selection()
        # esci da Muovi punto se attivo
        self.termina_muovi_punto()

        if not getattr(self, "database_traccia", None):
            return

        def _apply(i_sel):
            if i_sel is None:
                QMessageBox.information(self, "Selezione mancante",
                                        "Seleziona un punto sulla mappa per aggiungere un waypoint.")
                return

            i = int(i_sel)
            if i < 0 or i >= len(self.database_traccia):
                QMessageBox.warning(self, "Indice non valido",
                                    f"Indice selezionato {i} fuori range.")
                return

            # se già waypoint, non fare doppioni
            if self.database_traccia[i].get("waypoint_nota") is not None:
                pass
            else:
                # attiva come waypoint (segna; poi rinumeriamo tutti)
                self.database_traccia[i]["waypoint_nota"] = 0
                self.database_traccia[i]["waypoint"] = 0

            # ROAD TYPE: ereditarietà tra waypoints
            self._init_road_types_for_new_waypoint(i)
            self._propagate_in_from(i)


            # rinumerazione coerente 1..N (ordine di indice) + copia su 'waypoint'
            self._rinumera_waypoints()
            # memorizza l'ultima nota aggiunta (dopo rinumerazione)
            self._recent_note_num = int(self.database_traccia[i].get("waypoint_nota") or 0)
            self._current_note_num = self._recent_note_num


            # contatori/UI base
            self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
            if hasattr(self, "lbl_note"):
                self.lbl_note.setText(str(self.note_totali))
            self.file_modificato_flag = True

            # invia a mappa (una sola fonte)
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()
            self.aggiorna_status(f"Aggiunto waypoint sul punto {i}")

            # >>> refresh note DOPO l'update asincrono
            from PySide6.QtCore import QTimer
            QTimer.singleShot(0, lambda: self._refresh_notes(scroll_to_idx=i))

        # Legge l'indice selezionato dalla mappa (callback)
        self._get_selected_index_from_map(_apply)

    #                                                                                                                MUOVI PUNTO "M"
    # Muovi punto M
    def muovi_punto(self):
        """Muove il punto selezionato: un click nuovo e si chiude subito."""
        def _start(i_sel):
            if i_sel is None:
                QMessageBox.information(self, "Seleziona un punto",
                                        "Clicca un punto sulla mappa (giallo) e riprova M.")
                return

            self._move_mode_active = True
            self._move_busy = False

            # Shortcut Return per annullare senza spostare
            try:
                self._shortcut_move_exit.setParent(None)
            except Exception:
                pass
            self._shortcut_move_exit = QShortcut(QKeySequence("Return"), self)
            self._shortcut_move_exit.activated.connect(self.termina_muovi_punto)

            self.statusBar().showMessage("Modalità Muovi punto attiva — clicca nuova posizione o premi Invio per annullare.", 0)

            # Blink JS
            self.web.page().runJavaScript("window.moveModeStart()", 0)

            # Timer di polling
            if getattr(self, "_move_timer", None):
                try: self._move_timer.stop()
                except Exception: pass
            self._move_timer = QTimer(self)
            self._move_timer.setInterval(150)
            self._move_timer.timeout.connect(self._move_poll)
            self._move_timer.start()

        self._get_selected_index_from_map(_start)

    def _move_poll(self):
        """Poll del target scelto in JS."""
        if getattr(self, "_move_busy", False): return
        if not getattr(self, "_move_mode_active", False): return
        self._move_busy = True
        self.web.page().runJavaScript(
            "(function(){var t=window.readMoveTarget();return t?JSON.stringify(t):null})()",
            0,
            self._on_move_target_json
        )

    def _on_move_target_json(self, s):
        """Riceve nuovo target e applica subito lo spostamento."""
        if not s:
            self._move_busy = False
            return
        try:
            lon, lat = json.loads(s)
            lon = float(lon); lat = float(lat)
        except Exception:
            self._move_busy = False
            return

        def _apply(i_sel):
            if i_sel is None:
                self._move_busy = False
                return
            i = int(i_sel)
            if not (0 <= i < len(self.database_traccia)):
                self._move_busy = False
                return

            # aggiorna DB
            self.database_traccia[i]["lat_trackpoint"] = lat
            self.database_traccia[i]["lon_trackpoint"] = lon
            self._update_caps_after_move(i)
            self.file_modificato_flag = True
            self.aggiorna_km_totali()
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()
            QTimer.singleShot(0, self._refresh_notes)

            # ri-seleziona e pulisci target in JS
            js = f"""
            (function(i){{
            const src = map.getSource('track-points');
            const pts = src ? src._data?.features : null;
            if (Array.isArray(pts) && pts[i]) {{
                map.getSource('selected-point').setData({{type:'FeatureCollection', features:[pts[i]]}});
            }}
            window.clearMoveTarget && window.clearMoveTarget();
            }})({i});
            """
            self.web.page().runJavaScript(js, 0)

            # svuota selezione (con guard)
            self.web.page().runJavaScript(
                "(()=>{const s=map.getSource('selected-point'); if(s){s.setData({type:'FeatureCollection',features:[]});}})()",
                0
            )

            # chiudi modalità e status
            self.termina_muovi_punto()
            self.aggiorna_status(f"Spostato punto {i}")
            

        self._get_selected_index_from_map(_apply)

    def termina_muovi_punto(self):
        """Esce dalla modalità Muovi punto (senza spostare se nulla selezionato)."""
        if not getattr(self, "_move_mode_active", False):
            return

        # disattiva modalità e timer
        self._move_mode_active = False
        if getattr(self, "_move_timer", None):
            try: self._move_timer.stop()
            except Exception: pass
            self._move_timer = None
        self._move_busy = False

        # rimuovi scorciatoia Return se presente
        if getattr(self, "_shortcut_move_exit", None):
            try: self._shortcut_move_exit.setParent(None)
            except Exception: pass
            self._shortcut_move_exit = None

        # UI + JS: stop blink/handler e spegni selezione
        self.statusBar().clearMessage()

        # eviti errori se le funzioni/sorgenti non esistono ancora
        self.web.page().runJavaScript(
            "window.moveModeStop && window.moveModeStop()", 0
        )
        self.web.page().runJavaScript(
            "(()=>{const s=map.getSource('selected-point'); if(s){s.setData({type:'FeatureCollection',features:[]});}})()",
            0
        )

    #                                                                                                         AGGIUNGI PUNTO INTERMEDIO "A"
    # Agiungi punto intermedio A
    def aggiungi_punto_intermedio(self):
        # esci da Muovi punto se attivo
        self.termina_muovi_punto()

        if not getattr(self, "database_traccia", None):
            return

        def _apply(i_sel):
            if i_sel is None:
                self.aggiorna_status("Seleziona un punto e evidenzialo (giallo) e riprova: A")
                return

            i = int(i_sel)
            if i < 0 or i >= len(self.database_traccia)-1:
                self.aggiorna_status("Il punto selezionato deve avere un successivo (A)")
                return

            p0 = self.database_traccia[i]
            p1 = self.database_traccia[i+1]
            lat_new = (p0["lat_trackpoint"] + p1["lat_trackpoint"]) / 2
            lon_new = (p0["lon_trackpoint"] + p1["lon_trackpoint"]) / 2

            nuovo = {
                "indice": i+1,
                "waypoint_nota": None,
                "lat_trackpoint": lat_new,
                "lon_trackpoint": lon_new,
                "road_type_in": p0.get("road_type_out", 0),
                "road_type_out": p0.get("road_type_out", 0)
            }

            # inserisci nel DB
            self.database_traccia.insert(i+1, nuovo)

            # re-indicizza tutto
            for idx, p in enumerate(self.database_traccia):
                p["indice"] = idx

            # dopo la re-indicizzazione
            if i+2 < len(self.database_traccia):
                self.database_traccia[i+2]["road_type_in"] = nuovo["road_type_out"]

            # aggiorna UI e mappa
            self.file_modificato_flag = True
            self.num_punti_traccia = len(self.database_traccia)
            self.aggiorna_km_totali()
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()

            QTimer.singleShot(0, self._refresh_notes)

            # Aggiorna Status Bar
            self.aggiorna_status(f"Inserito punto intermedio tra {i} e {i+1}")

        self._get_selected_index_from_map(_apply)

    #                                                                                                          AGGIUNGI PUNTO SU LINEA "L"
    # Aggiungipunto su linea L
    def aggiungi_punto_su_linea(self):
        self.aggiorna_status("Ctrl+L: lettura cursore…")
        self.termina_muovi_punto()
        if not getattr(self, "database_traccia", None) or len(self.database_traccia) < 2:
            return

        def _on_cursor(js_value):
            if not js_value:
                self.aggiorna_status("Muovi il mouse sopra la mappa e riprova: L")
                return
            try:
                lon, lat = json.loads(js_value)
                lon = float(lon); lat = float(lat)
            except Exception:
                return

            # distanza punto-segmento (metri)
            def dist_point_to_segment_m(lat, lon, a, b):
                latr = math.radians(lat); lonr = math.radians(lon)
                lat1 = math.radians(a["lat_trackpoint"]); lon1 = math.radians(a["lon_trackpoint"])
                lat2 = math.radians(b["lat_trackpoint"]); lon2 = math.radians(b["lon_trackpoint"])
                dx = lon2 - lon1; dy = lat2 - lat1
                if dx == 0 and dy == 0:
                    return math.hypot(latr - lat1, lonr - lon1) * 6371000.0
                t = ((lonr - lon1)*dx + (latr - lat1)*dy) / (dx*dx + dy*dy)
                t = 0.0 if t < 0.0 else (1.0 if t > 1.0 else t)
                proj_lon = lon1 + t*dx; proj_lat = lat1 + t*dy
                return math.hypot(latr - proj_lat, lonr - proj_lon) * 6371000.0

            pts = self.database_traccia
            best_k = 0
            best_d = float("inf")
            for k in range(len(pts)-1):
                d = dist_point_to_segment_m(lat, lon, pts[k], pts[k+1])
                if d < best_d:
                    best_d = d
                    best_k = k

            # soglia di accettazione                                                  QUI PUOI CAMBIARE LA SOGLIA (IN METRI) OLTRE ALLA QUALE LA LINEA NON SARÀ PIÙ RICONOSCIUTA
            threshold_m = 15.0
            if best_d > threshold_m:
                self.aggiorna_status(f"Troppo lontano dalla linea (> {threshold_m:.0f} m). Avvicina il cursore e riprova: L")
                return

            # inserisci DOPO k (segmento ≠ punto)
            nuovo = {
                "indice": best_k+1,
                "waypoint_nota": None,
                "lat_trackpoint": lat,
                "lon_trackpoint": lon,
                "road_type_in": pts[best_k].get("road_type_out", 0),
                "road_type_out": pts[best_k].get("road_type_out", 0)
            }
            self.database_traccia.insert(best_k+1, nuovo)

            # re-indicizza e continuità road_type per il successivo
            for idx, p in enumerate(self.database_traccia):
                p["indice"] = idx
            if best_k+2 < len(self.database_traccia):
                self.database_traccia[best_k+2]["road_type_in"] = nuovo["road_type_out"]

            # aggiorna UI/mappa
            self.file_modificato_flag = True
            self.num_punti_traccia = len(self.database_traccia)
            self.aggiorna_km_totali()
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()

            # refresh note DOPO l'update asincrono
            QTimer.singleShot(0, self._refresh_notes)

            self.aggiorna_status(f"Inserito punto su linea dopo segmento {best_k}")

        js = "(function(){ try{return window.readCursorLngLat? window.readCursorLngLat(): null;}catch(e){return null;} })()"
        self.web.page().runJavaScript(js, 0, _on_cursor)

    #                                                                                                    TRASFORMA WAYPOINT IN TRACKPOINT "T"

    # Trasforma trackpoint in waypoint T
    def trasforma_waypoint_in_trackpoint(self):
        # esci da Muovi punto se attivo
        self.termina_muovi_punto()
        if not getattr(self, "database_traccia", None):
            return

        def _apply(i_sel):
            if i_sel is None:
                self.aggiorna_status("Seleziona un waypoint (rosso) e riprova: T")
                return

            try:
                i = int(i_sel)
            except Exception:
                self.aggiorna_status("Indice selezionato non valido (T)")
                return

            n = len(self.database_traccia)
            count_wp = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)

            # vincoli: più di 2 waypoint, e non primo/ultimo
            if count_wp <= 2:
                self.aggiorna_status("Operazione non consentita: presenti solo inizio/fine")
                return
            if i <= 0 or i >= n-1:
                self.aggiorna_status("Non consentito sul primo o sull'ultimo waypoint (T)")
                return
            if self.database_traccia[i].get("waypoint_nota") is None:
                self.aggiorna_status(f"Il punto {i} non è un waypoint")
                return

            # rimuovi ruolo di waypoint
            self.database_traccia[i]["waypoint_nota"] = None
            self.database_traccia[i].pop("waypoint", None)

            # rinumerazione coerente e UI
            self._rinumera_waypoints()
            self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
            if hasattr(self, "lbl_note"):
                self.lbl_note.setText(str(self.note_totali))

            self.file_modificato_flag = True
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()

            # refresh note DOPO l'update asincrono
            QTimer.singleShot(0, self._refresh_notes)

            self.aggiorna_status(f"Trasformato waypoint in trackpoint (idx {i})")

        self._get_selected_index_from_map(_apply)


    #                                                                                                              CANCELLA PUNTO "DEL"
    # Cancella punto DEL
    def cancella_punto(self):
        self.termina_muovi_punto()

        if not getattr(self, "database_traccia", None) or len(self.database_traccia) < 3:
            self.aggiorna_status("Servono almeno 3 punti per cancellarne uno")
            return

        def _apply(i_sel):
            if i_sel is None:
                self.aggiorna_status("Seleziona un punto (giallo) e riprova: Del")
                return
            try:
                i = int(i_sel)
            except Exception:
                self.aggiorna_status("Indice selezionato non valido (Del)")
                return
            n = len(self.database_traccia)
            if not (0 <= i < n):
                self.aggiorna_status("Indice fuori range (Del)")
                return

            # non cancellare primo/ultimo
            if i == 0 or i == n-1:
                self.aggiorna_status("Il primo e l'ultimo punto non possono essere cancellati")
                return

            # continuità road type: il successivo eredita l'out del precedente
            prevp = self.database_traccia[i-1] if i-1 >= 0 else None
            nextp = self.database_traccia[i+1] if i+1 < n else None
            if prevp is not None and nextp is not None:
                nextp["road_type_in"] = prevp.get("road_type_out", nextp.get("road_type_in", 0))

            # conferma
            reply = QMessageBox.question(
                self,
                "Conferma",
                "Vuoi davvero cancellare il punto selezionato?",
                QMessageBox.Yes | QMessageBox.No
            )
            if reply == QMessageBox.No:
                return

            # rimuovi
            self.database_traccia.pop(i)


            # re-indicizza
            for idx, p in enumerate(self.database_traccia):
                p["indice"] = idx

            # assicurati che primo/ultimo siano waypoint (placeholder → poi rinumero)
            if len(self.database_traccia) >= 2:
                if self.database_traccia[0].get("waypoint_nota") is None:
                    self.database_traccia[0]["waypoint_nota"] = 0
                    self.database_traccia[0]["waypoint"] = 0
                if self.database_traccia[-1].get("waypoint_nota") is None:
                    self.database_traccia[-1]["waypoint_nota"] = 0
                    self.database_traccia[-1]["waypoint"] = 0

            # rinumera waypoint + UI
            self._rinumera_waypoints()
            self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota") is not None)
            if hasattr(self, "lbl_note"):
                self.lbl_note.setText(str(self.note_totali))

            #                                                                                         Propagazione tipi strada a valle
            # dopo il pop(), l'indice di partenza è il punto a sinistra del buco
            start_idx = min(i, len(self.database_traccia) - 1)
            # trova il waypoint precedente da cui far partire la catena
            prev_wp = None
            for j in range(start_idx, -1, -1):
                if self.database_traccia[j].get("waypoint_nota") is not None:
                    prev_wp = j
                    break
            if prev_wp is not None:
                self._propagate_in_from(prev_wp)
            
            self.file_modificato_flag = True
            self.num_punti_traccia = len(self.database_traccia)
            self.aggiorna_km_totali()
            self.update_menu_traccia()
            self.invia_traccia_a_mappa()

            # >>> refresh note DOPO l'update asincrono
            QTimer.singleShot(0, self._refresh_notes)

            self.aggiorna_status(f"Cancellato punto {i}")

        self._get_selected_index_from_map(_apply)


    #                                                                                                          COMUNICAZIONE CON LA MAPPA

    #                                                                                               CALLBACK PER MAP GET SOURCES DA DATABASE_TRACCIA-

    # --- Lettura indice selezionato dalla mappa (callback o return), con retry ---
    def _get_selected_index_from_map(self, cb=None, retries=5, interval_ms=60):
        """
        Legge l'indice da 'selected-point'.
        - Se cb è passato: invoca cb(i) quando disponibile (con piccoli retry).
        - Se cb è None: ritorna i (blocca con mini event loop).
        """
        js = """
        (() => {
        try {
            const s = map.getSource('selected-point');
            const f = s && s._data && s._data.features && s._data.features[0];
            if (!f || !f.properties) return null;
            const v = (f.properties.id ?? f.properties.idx ?? f.properties.i ?? null);
            const n = Number(v);
            return Number.isFinite(n) ? n : null;
        } catch(e) { return null; }
        })();
        """

        page_holder = getattr(self, "web", None) or getattr(self, "webview", None) or getattr(self, "view", None)
        page = page_holder.page() if hasattr(page_holder, "page") else page_holder
        if not page:
            if cb: cb(None)
            return None

        # --- ramo asincrono (con callback) ---
        if cb:
            def attempt(left):
                page.runJavaScript(
                    js, 0,
                    lambda v: cb(v) if (v is not None or left <= 0)
                    else QTimer.singleShot(interval_ms, lambda: attempt(left - 1))
                )
            attempt(retries)
            return

        # --- ramo sincrono (con retry) ---
        out = {"i": None}
        loop = QEventLoop()

        def attempt_sync(left):
            page.runJavaScript(
                js, 0,
                lambda v: (
                    out.__setitem__("i", v), loop.quit()
                ) if (v is not None or left <= 0)
                else QTimer.singleShot(interval_ms, lambda: attempt_sync(left - 1))
            )

        attempt_sync(retries)
        loop.exec()
        return out["i"]



    #                                                                                                     INVIA DATABASE_TRACCIA-A MAP GET SOURCE
    # Invia il database_traccia alla mappa per diseegnare polilinea e punti.
    def invia_traccia_a_mappa(self):
        if not getattr(self, "database_traccia", None):
            return
        payload = [{
            "lat": p["lat_trackpoint"],
            "lon": p["lon_trackpoint"],
            "flag": bool(p["waypoint_nota"])
        } for p in self.database_traccia]
        js = f"window.loadTrack({json.dumps(payload)})"
        self.web.page().runJavaScript(js)

    #                                                                                                           RIORDINO WAYPOINT E INDICE
    def _rinumera_waypoints(self):
        """
        Assegna waypoint_nota = 1..N nell'ordine di indice,
        e copia lo stesso numero anche in campo 'waypoint'.
        I punti senza waypoint restano con None.
        """
        # prendi tutti gli indici che hanno un waypoint_nota (o che hanno il campo waypoint attivo)
        indices_wp = [i for i,p in enumerate(self.database_traccia) if p.get("waypoint_nota") is not None]
        # ordina per indice crescente
        indices_wp.sort()
        # assegna numeri consecutivi
        for order, idx in enumerate(indices_wp, start=1):
            self.database_traccia[idx]["waypoint_nota"] = order
            self.database_traccia[idx]["waypoint"] = order


    #                                                                                                               FUNZIONI ROADBOOK

    # --- util per fare un nome-file dal titolo
    def _slug(self, s: str) -> str:
        import re
        s = (s or "").strip().lower()
        s = re.sub(r'[^a-z0-9]+', '-', s)
        return s.strip('-') or 'roadbook'

    #                                                                                                            CARICA ROADBOOK CTRL+O
    # Carica un file json (roadbook completo: track + notes)
    def apri_roadbook(self):
        self.roadbook_caricato = True
        if self.file_modificato_flag:
            if not self.warning_chiudiSovrascrivi():
                return
        try:
            from PySide6.QtWidgets import QFileDialog
            import json, os

            path, _ = QFileDialog.getOpenFileName(self, "Apri Roadbook", getattr(self, "last_roadbook_path", ""), "Roadbook JSON (*.json)")
            if not path:
                return

            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)

            track = data.get("track") or []
            notes = data.get("notes") or []
            if not track:
                self.aggiorna_status("Roadbook senza traccia."); return

            # --- ricostruisci database_traccia come in carica_gpx ---
            db = []
            for i, tp in enumerate(track):
                try:
                    lat = float(tp.get("lat"))
                    lon = float(tp.get("lon") if tp.get("lon") is not None else tp.get("lng"))
                except Exception:
                    continue
                db.append({
                    "indice": i,
                    "waypoint_nota": None,           # li impostiamo dalle notes
                    "lat_trackpoint": lat,
                    "lon_trackpoint": lon,
                    "road_type_in": 0,
                    "road_type_out": 0,
                })

            # applica dati note sull'indice corrispondente
            for n in notes:
                try:
                    idx = int(n.get("idx"))
                except Exception:
                    continue
                if 0 <= idx < len(db):
                    p = db[idx]
                    num = n.get("num")
                    if num is not None:
                        p["waypoint_nota"] = num
                        p["waypoint"] = num
                    p["testo"] = n.get("testo", "")
                    p["cap_hdr"] = n.get("cap_hdr")
                    p["cap_km"] = n.get("cap_km")
                    p["hide_cap_btm"] = bool(n.get("hide_cap_btm", False))
                    if n.get("road_type_in") is not None:
                        p["road_type_in"] = n.get("road_type_in")
                    if n.get("road_type_out") is not None:
                        p["road_type_out"] = n.get("road_type_out")
                    p["bivio"] = n.get("bivio")
                    p["icone"] = n.get("icons") or []
                    # normalizza icone: name + path reale risolto
                    for ic in p["icone"]:
                        if not isinstance(ic, dict):
                            continue
                        fn = ic.get("file") or ic.get("path") or ""
                        base = (ic.get("name") or os.path.basename(str(fn)) or "").strip()
                        ic["name"] = base
                        real = resolve_icon_path(base)
                        if real:
                            ic["file"] = real

            # --- stato interno + UI ---
            self.database_traccia = db
            self.num_punti_traccia = len(db)
            self._note_widgets_by_num.clear()
            self._note_idx_by_num.clear()
            self.last_roadbook_path = path
            self.file_modificato_flag = False
            # titolo (se presente nel meta) + fallback al nome file
            meta = data.get("meta") or {}
            titolo = meta.get("titolo") or meta.get("title") or os.path.splitext(os.path.basename(path))[0]
            if hasattr(self, "title_edit"):
                self.title_edit.setText(titolo)
            self.setWindowTitle(f"RoadBook Editor — {titolo}")

            # logo: se il path salvato esiste lo usa, altrimenti torna al logo di default
            logo_path = meta.get("logo_path", "")

            if isinstance(logo_path, str) and logo_path.strip() and os.path.exists(logo_path):
                p = QPixmap(logo_path)
                if not p.isNull():
                    self.logo_path = logo_path
                    self.logo_pix = p
                    h = max(1, self.logo.height())
                    self.logo.setPixmap(self.logo_pix.scaledToHeight(h, Qt.SmoothTransformation))
                else:
                    p = QPixmap(self.default_logo_path)
                    if not p.isNull():
                        self.logo_path = self.default_logo_path
                        self.logo_pix = p
                        h = max(1, self.logo.height())
                        self.logo.setPixmap(self.logo_pix.scaledToHeight(h, Qt.SmoothTransformation))
            else:
                p = QPixmap(self.default_logo_path)
                if not p.isNull():
                    self.logo_path = self.default_logo_path
                    self.logo_pix = p
                    h = max(1, self.logo.height())
                    self.logo.setPixmap(self.logo_pix.scaledToHeight(h, Qt.SmoothTransformation))

            # km/note + menu
            self.aggiorna_km_totali()
            self.note_totali = sum(1 for p in self.database_traccia if p.get("waypoint_nota"))
            if hasattr(self, "lbl_note"):
                self.lbl_note.setText(str(self.note_totali))
            self.update_menu_traccia()

            # mappa: invia payload come fa il GPX
            self.invia_traccia_a_mappa()
            if hasattr(self, "web") and hasattr(self.web, "page"):
                self.web.page().runJavaScript(
                    "map && map.once('idle', ()=> window.recenterFirst && window.recenterFirst())", 0
                )

            # ricostruisci note
            self.rebuild_notes_from_db()
            self.highlight_note_by_selected_point()

            self.statusBar().showMessage(self.t("status.opened", name=os.path.basename(path)) + "  |  " + self.t("status.notes", n=self.note_totali), 2000)

        except Exception as e:
            self.aggiorna_status(self.t("error.open", err=e))

    #                                                                                                            SALVA ROADBOOK CTRL+S
    
    # Salva il roadbook in JSON (tutti i dati necessari alla renderizzazione)
    def salva_roadbook(self):
        try:
            import json
            from PySide6.QtWidgets import QFileDialog

            # 1) scegli percorso (suggerisci titolo)
            import os
            titolo = (self.title_edit.text().strip() if hasattr(self, "title_edit") else "")
            directory = os.path.dirname(getattr(self, "last_roadbook_path", "") or "") or os.getcwd()
            default_name = os.path.join(directory, f"{self._slug(titolo)}.json")
            path, _ = QFileDialog.getSaveFileName(self, "Salva Roadbook", default_name, "Roadbook JSON (*.json)")
            if not path:
                return

            # 2) metriche (progressivo/parziale per WAYPOINT)
            metrics = self._compute_waypoint_metrics()  # [{idx,num,km_prog,km_parz}, ...]

            # 3) header/meta minimi
            meta = {
                "titolo": titolo,
                "km_totali": getattr(self, "km_val_lbl", None).text() if hasattr(self, "km_val_lbl") else "",
                "note_count": len(metrics),
            }

            # 3a salva il percorso del logo selezionato, se presente
            lp = getattr(self, "logo_path", "")
            if isinstance(lp, str) and lp.strip():
                meta["logo_path"] = lp

            # 4) note serializzate (tutto pronto per disegno, niente calcoli in app mobile)
            notes = []
            for m in metrics:
                idx = m["idx"]
                p = self.database_traccia[idx]

                # coordinate waypoint
                lat = p.get("lat_trackpoint")
                lon = p.get("lon_trackpoint")

                # CAP rosso (persistente, tra waypoint idx -> prossimo waypoint)
                cap_hdr = p.get("cap_hdr")
                cap_km  = p.get("cap_km")

                # CAP piccolo (bearing verso trackpoint successivo)
                cap_small = None
                if idx + 1 < len(self.database_traccia):
                    try:
                        cap_small = float(self._bearing_deg(self.database_traccia[idx], self.database_traccia[idx+1]))
                    except Exception:
                        cap_small = None

                # Bearing IN/OUT e stili strada
                try:
                    b_in, b_out = self._bearing_in_out(idx)
                except Exception:
                    b_in, b_out = None, None
                road_type_in  = p.get("road_type_in")
                road_type_out = p.get("road_type_out")

                # Bivio e icone di colonna B
                bivio = p.get("bivio")

                # ICONS: salva con path relativo "icons/<filename>"
                icons_src = p.get("icone") or p.get("icons")
                icons = []
                if icons_src:
                    import os
                    for ic in icons_src:
                        if not isinstance(ic, dict):
                            continue
                        ic2 = dict(ic)
                        fn = ic.get("file") or ic.get("path") or ""
                        base = os.path.basename(str(fn))
                        ic2["name"] = base
                        ic2["file"] = f"icons/{base}" if base else ""
                        icons.append(ic2)

                # Testo descrizione
                testo = p.get("testo", "")

                # Visibilità CAP bottom
                hide_cap_btm = bool(p.get("hide_cap_btm", False))

                notes.append({
                    "num": m["num"],
                    "idx": idx,
                    "km_prog": float(m["km_prog"]) if m.get("km_prog") is not None else None,
                    "km_parz": float(m["km_parz"]) if m.get("km_parz") is not None else None,
                    "lat": float(lat) if lat is not None else None,
                    "lon": float(lon) if lon is not None else None,
                    "testo": testo,
                    "cap_hdr": float(cap_hdr) if cap_hdr is not None else None,    # gradi
                    "cap_km": float(cap_km) if cap_km is not None else None,       # km in linea d'aria
                    "cap_small": float(cap_small) if cap_small is not None else None,  # gradi
                    "hide_cap_btm": hide_cap_btm,
                    "bearing_in": float(b_in) if b_in is not None else None,
                    "bearing_out": float(b_out) if b_out is not None else None,
                    "road_type_in": road_type_in,
                    "road_type_out": road_type_out,
                    "bivio": bivio,          # struttura già pronta per il tuo canvasB
                    "icons": icons,          # lista di icone con pos/rot/size salvate
                })

            # esporta tutta la traccia (trackpoints)
            track = []
            for p in self.database_traccia:
                lat = p.get("lat_trackpoint"); lon = p.get("lon_trackpoint")
                if lat is None or lon is None:
                    continue
                track.append({"lat": float(lat), "lon": float(lon)})

            payload = {
                "meta": meta,
                "notes": notes,
                "track": track,   # <— aggiunto
            }

            # 5) scrivi file
            with open(path, "w", encoding="utf-8") as f:
                json.dump(payload, f, ensure_ascii=False, indent=2)

            self.last_roadbook_path = path
            self.file_modificato_flag = False
            self.statusBar().showMessage(self.t("status.roadbook_saved"), 2000)

        except Exception as e:
            self.aggiorna_status(self.t("error.save", err=e))

    # Esporta il pacchetto digitale per il lettore
    def esporta_digitale(self):
        try:
            import os, json, shutil, time
            from pathlib import Path
            from PySide6.QtWidgets import QMessageBox, QFileDialog

            # Se ci sono modifiche non salvate, chiedi cosa fare
            if getattr(self, "file_modificato_flag", False):
                r = QMessageBox.question(
                    self,
                    "Esporta digitale",
                    "Il roadbook è stato modificato.\nVuoi salvare prima di esportare?",
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
                )
                if r == QMessageBox.Cancel:
                    return
                if r == QMessageBox.Yes:
                    self.salva_roadbook()

            json_path = getattr(self, "last_roadbook_path", "") or ""
            if not json_path or not os.path.isfile(json_path):
                QMessageBox.warning(self, "Esporta digitale", "Prima salva il roadbook (CTRL+S).")
                return

            # Carica JSON
            with open(json_path, "r", encoding="utf-8") as f:
                data = json.load(f)

            # Titolo per nome zip
            titolo = ""
            try:
                titolo = (data.get("meta", {}).get("titolo") or data.get("meta", {}).get("title") or "").strip()
            except Exception:
                titolo = ""
            if not titolo:
                titolo = os.path.splitext(os.path.basename(json_path))[0]
            slug = self._slug(titolo)

            base_dir = os.path.dirname(os.path.abspath(json_path))

            # Scegli dove salvare lo ZIP (default: stessa cartella del .json)
            default_zip = os.path.join(base_dir, f"{slug}_RB_Reader_pack.zip")
            zip_path, _ = QFileDialog.getSaveFileName(
                self,
                "Esporta digitale (ZIP per RB Reader)",
                default_zip,
                "ZIP (*.zip)"
            )
            if not zip_path:
                return
            if not zip_path.lower().endswith(".zip"):
                zip_path += ".zip"

            # Cartella NON compressa (utile per legenda icone)
            out_dir = os.path.join(os.path.dirname(zip_path), "RB_Reader_pack")
            icons_out = os.path.join(out_dir, "icons")
            out_json = os.path.join(out_dir, "rb.json")

            # Conferma sovrascrittura cartella output
            if os.path.isdir(out_dir):
                rr = QMessageBox.question(
                    self,
                    "Esporta digitale",
                    f"La cartella '{out_dir}' esiste già.\nVuoi rigenerarla?",
                    QMessageBox.Yes | QMessageBox.No
                )
                if rr != QMessageBox.Yes:
                    return
                shutil.rmtree(out_dir, ignore_errors=True)

            os.makedirs(icons_out, exist_ok=True)

            IMG_EXT = {".png", ".webp", ".svg", ".jpg", ".jpeg"}

            # Raccogli icone usate nel roadbook
            icon_names = set()
            for note in data.get("notes", []) or []:
                for ic in note.get("icons", []) or []:
                    file_field = ic.get("file") or ic.get("name")
                    if not file_field:
                        continue
                    base = os.path.basename(str(file_field)).strip()
                    if not base:
                        continue
                    if os.path.splitext(base)[1].lower() in IMG_EXT:
                        icon_names.add(base)
            icon_names = sorted(icon_names)

            # Cartelle dove cercare le icone (utente + exe + progetto)
            def _search_dirs():
                dirs = []
                try:
                    dirs.append(str(USER_ICONS))
                except Exception:
                    pass

                meipass = Path(getattr(sys, "_MEIPASS", os.getcwd()))
                dirs.append(str(meipass / "Icons"))
                dirs.append(str(meipass / "_internal" / "Icons"))

                here = Path(os.path.dirname(__file__))
                dirs.append(str(here / "Icons"))
                dirs.append(str(here / "_internal" / "Icons"))

                # fallback classico (se serve)
                dirs.append(r"C:\Program Files (x86)\RoadbookEditor\Icons")
                dirs.append(r"C:\Program Files (x86)\RoadbookEditor\_internal\Icons")
                return dirs

            # Indicizza file (case-insensitive)
            def _build_index(dirs):
                idx = {}
                for root_dir in dirs:
                    if not os.path.isdir(root_dir):
                        continue
                    for dirpath, _, files in os.walk(root_dir):
                        for fn in files:
                            ext = os.path.splitext(fn)[1].lower()
                            if ext in IMG_EXT:
                                k = fn.lower()
                                if k not in idx:
                                    idx[k] = os.path.join(dirpath, fn)
                return idx

            index = _build_index(_search_dirs())
            found, missing = [], []

            # Copia icone in minuscolo (Android è case-sensitive)
            for name in icon_names:
                k = name.lower()
                if k in index:
                    shutil.copyfile(index[k], os.path.join(icons_out, k))
                    found.append(name)
                else:
                    missing.append(name)

            # Riscrivi i riferimenti icone nel JSON -> "icons/<nome_lower>"
            out_data = json.loads(json.dumps(data))
            rewritten = 0
            for note in out_data.get("notes", []) or []:
                for ic in note.get("icons", []) or []:
                    file_field = ic.get("file") or ic.get("name")
                    if not file_field:
                        continue
                    base = os.path.basename(str(file_field)).strip()
                    if not base:
                        continue
                    lower = base.lower()
                    ic["file"] = f"icons/{lower}"
                    if base != lower:
                        rewritten += 1

            # Scrivi rb.json
            with open(out_json, "w", encoding="utf-8", newline="\n") as f:
                json.dump(out_data, f, ensure_ascii=False, indent=2)
                f.flush()
                try:
                    os.fsync(f.fileno())
                except Exception:
                    pass

            time.sleep(0.2)

            # Crea ZIP con dentro la cartella RB_Reader_pack
            parent = os.path.dirname(out_dir)
            base = os.path.basename(out_dir)
            tmp_zip = shutil.make_archive(out_dir, "zip", root_dir=parent, base_dir=base)

            # Sposta/rinomina nello zip scelto dall’utente
            try:
                if os.path.abspath(tmp_zip).lower() != os.path.abspath(zip_path).lower():
                    if os.path.exists(zip_path):
                        os.remove(zip_path)
                    os.replace(tmp_zip, zip_path)
            except Exception:
                # se non riesce a rinominare, almeno resta il tmp_zip
                zip_path = tmp_zip

            msg = (
                f"OK!\n\n"
                f"Cartella: {out_dir}\n"
                f"ZIP: {zip_path}\n\n"
                f"Icone richieste: {len(icon_names)}\n"
                f"Icone copiate: {len(found)}\n"
                f"Icone mancanti: {len(missing)}\n"
                f"Riferimenti riscritti (lower): {rewritten}"
            )
            if missing:
                msg += "\n\nMancanti (prime 15):\n" + "\n".join(" - " + m for m in missing[:15])

            QMessageBox.information(self, "Esporta digitale", msg)
            self.statusBar().showMessage("Esportazione digitale completata.", 2000)

        except Exception as e:
            QMessageBox.warning(self, "Esporta digitale", f"Errore esportazione:\n{e}")


    #                                                                                    MESSAGGIO AVVISO ESCI-SOVRASCRIVI PER POTER SALVARE E NON SOVRASCRIVERE
    # Mostra un messaggio di conferma se c'è una traccia già presente    # 
    def warning_chiudiSovrascrivi(self):
        msg = QMessageBox(self)
        msg.setIcon(QMessageBox.Warning)
        msg.setWindowTitle("Modifica in corso")
        msg.setText("Vuoi salvare le modifiche? ")
        msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)

        risposta = msg.exec()

        if risposta == QMessageBox.Yes:
            self.salva_roadbook()
            return True
        elif risposta == QMessageBox.No:
            return True
        else:
            return False
        
    #                                                                                                           COSTRUZIONE NOTE GRAFICHE  
    # --- Ricostruzione completa delle note dal database (full rebuild)
    def rebuild_notes_from_db(self):
        """Ricostruisce tutte le note da database_traccia (full rebuild)."""
        # 1) pulizia contenitore
        while self.note_v.count():
            item = self.note_v.takeAt(0)
            w = item.widget()
            if w:
                w.setParent(None)
        self._note_widgets_by_num.clear()
        self._note_idx_by_num.clear()

        # 2) dati (Passo 2: li calcoliamo da database_traccia)
        metrics = self._compute_waypoint_metrics()  # -> [{idx,num,km_prog,km_parz}, ...]

        # 3) crea righe
        fonts = {
            'prog': self.FONT_PROG,
            'parz': self.FONT_PARZ,
            'num' : self.FONT_NUM,
            'cap_btm': getattr(self, 'FONT_CAP_BOTTOM', 11),
            'coord_btm': getattr(self, 'FONT_COORD_BOTTOM', 10),
            'cap_hdr_scale': getattr(self, 'FONT_CAP_HDR_SCALE', 1.0),
            'cap_hdr_h': getattr(self, 'CAP_HDR_H', 32),
            'text_min_h': getattr(self, 'TEXT_MIN_H', 72),
            'text_max_len': self.TEXT_MAX_LEN,
        }
        sizes = {
            'note_h': self.NOTE_H,
            'col_a_w': self.COL_A_W,
            'col_b_w': self.COL_B_W,
        }

        for m in metrics:
            idx = m.get('idx')
            if idx is None:
                idx = self._note_idx_by_num.get(m['num'])
            if idx is None:
                try: idx = next(i for i,p in enumerate(self.database_traccia) if p.get('num') == m['num'])
                except StopIteration: continue
            p = self.database_traccia[idx]
            p.setdefault("testo", "")
            p.setdefault("cap_hdr", None)
            p.setdefault("hide_cap_btm", False)

            row = NoteRow(m['num'], m['km_prog'], m['km_parz'], fonts, sizes, self.on_click_column_A)
            row.set_short(m['km_parz'] <= self.SOGLIA_PARZIALE_SHORT_KM)

           # testo + CAP alto + visibilità CAP basso
            if hasattr(row, "desc_edit"):
                row.desc_edit.setPlainText(p.get("testo", ""))  # nessun placeholder
                row.desc_edit.textChanged.connect(lambda ed=row.desc_edit: self._fit_two_lines(ed))
            else:
                row.desc_lbl.setText(p.get("testo", ""))        # fallback se fosse ancora QLabel

            # CAP rosso (persistente) + km
            cap = p.get("cap_hdr")
            km  = p.get("cap_km")
            if cap is not None:
                if km is not None:
                    row.cap_hdr.setText(f"CAP {round(float(cap)):.0f}º  km {float(km):.2f}")
                else:
                    row.cap_hdr.setText(f"CAP {round(float(cap)):.0f}º")
                row.cap_hdr.setVisible(True)
            else:
                row.cap_hdr.clear()
                row.cap_hdr.setVisible(False)

            # CAP piccolo: visibile solo se non abbiamo nascosto il bottom
            row.cap_small.setVisible(not bool(p.get("hide_cap_btm", False)))

            # --- Vettori direzione colonna B ---
            idx = m['idx']
            b_in, b_out = self._bearing_in_out(idx)
            p = self.database_traccia[idx]

            # stile scelto globalmente (bottone) oppure default
            chosen = getattr(self, 'current_pen_style', None) or road_types[0]

            def _rt(i):
                try:
                    i = int(i)
                    return road_types[i] if 0 <= i < len(road_types) else chosen
                except Exception:
                    return chosen

            style_in  = _rt(p.get("road_type_in",  None))
            style_out = _rt(p.get("road_type_out", None))

            if hasattr(row, "canvasB"):
                row.canvasB.set_bearings(b_in, b_out, style_in, style_out)
                row.canvasB.on_clicked = (lambda n=m['num']: self.on_click_vector_out(n))
                row.canvasB.on_bivio_clicked = (lambda n=m['num']: self.on_click_bivio(n))
                row.canvasB.on_blank_clicked  = self.on_blank_canvas_click
                row.canvasB.on_bivio_clicked  = (lambda n=m['num']: self.on_click_bivio(n))
                row.canvasB.on_blank_clicked  = self.on_blank_canvas_click
                row.canvasB.on_bivio_changed  = (lambda d, n=m['num']: self.on_bivio_changed(n, d))

                # step 1: applica eventuale bivio salvato su questa nota
                row.canvasB.set_bivio(p.get("bivio"))

                # NEW: ricarica anche le icone dal database per questo canvas
                row.canvasB.icons = p.get("icone") or p.get("icons") or []
                row.canvasB.update()
                # --- Colonna C: CAP basso e coordinate ---
                # CAP = bearing dal waypoint (idx) al trackpoint successivo
                txt_cap = "--º"
                if idx + 1 < len(self.database_traccia):
                    try:
                        cap_btm = self._bearing_deg(self.database_traccia[idx], self.database_traccia[idx+1])
                        txt_cap = f"{round(cap_btm):.0f}º"
                    except Exception:
                        pass
                if hasattr(row, "cap_small"):
                    row.cap_small.setText(txt_cap)

                # Coordinate in dd.dddddd
                lat = p.get("lat_trackpoint"); lon = p.get("lon_trackpoint")
                if lat is not None and lon is not None and hasattr(row, "coord_lbl"):
                    try:
                        row.coord_lbl.setText(f"{float(lat):.6f}º    {float(lon):.6f}º")
                    except Exception:
                        row.coord_lbl.setText("—    —")

            if m['num'] == self._recent_note_num and hasattr(row, "set_recent"):
                row.set_recent(True)


            self.note_v.addWidget(row)
            self._note_widgets_by_num[m['num']] = row
            self._note_idx_by_num[m['num']] = m['idx']


        self.note_v.addStretch(1)
        # 4) evidenzia/scroll su selezionata (Passo 2 collegherà la selezione reale)
        self.highlight_note_by_selected_point()

    # --- (stub) Calcolo progressivo, parziale  e numero nota dagli Waypoint del database
    def _compute_waypoint_metrics(self):
        """
        NOTE = waypoints con waypoint_nota>0 O waypoint>0 (anche come stringhe).
        Ordine = posizione nel vettore traccia (non il numero). Rinumerazione 1..N.
        km_prog = cumulata lungo tutti i trackpoint; km_parz = diff tra note adiacenti.
        """
        pts = getattr(self, "database_traccia", [])
        if not pts or len(pts) < 2:
            return []

        # mappa indice->posizione (robusta se 'indice' non è compatto)
        pos_by_indice = {int(p["indice"]): i for i, p in enumerate(pts)}

        # helper: true se v è numero >0 (accetta stringhe)
        def is_pos_num(v):
            try:
                return float(v) > 0
            except (TypeError, ValueError):
                return False

        # prendi indici dei waypoint (waypoint_nota o waypoint)
        wp_indices = [
            int(p["indice"]) for p in pts
            if is_pos_num(p.get("waypoint_nota")) or is_pos_num(p.get("waypoint"))
        ]
        if not wp_indices:
            return []

        # ordina lungo la traccia
        wp_indices.sort(key=lambda idx: pos_by_indice.get(idx, idx))

        # cumulata Haversine (km) sui trackpoint
        from math import radians, sin, cos, asin, sqrt
        def hav_km(a, b):
            R = 6371.0
            dφ = radians(b["lat_trackpoint"] - a["lat_trackpoint"])
            dλ = radians(b["lon_trackpoint"] - a["lon_trackpoint"])
            φ1 = radians(a["lat_trackpoint"]); φ2 = radians(b["lat_trackpoint"])
            s = sin(dφ/2)**2 + cos(φ1)*cos(φ2)*sin(dλ/2)**2
            return 2*R*asin(sqrt(s))

        cum = [0.0] * len(pts)
        for i in range(1, len(pts)):
            cum[i] = cum[i-1] + hav_km(pts[i-1], pts[i])

        # output rinumerato 1..N
        out, prev_km = [], None
        for n, idx in enumerate(wp_indices, start=1):
            pos = pos_by_indice[idx]
            km_prog = cum[pos]
            km_parz = 0.0 if prev_km is None else max(0.0, km_prog - prev_km)
            out.append({"idx": idx, "num": n, "km_prog": km_prog, "km_parz": km_parz})
            prev_km = km_prog
        return out
    
    # --- Bearings (gradi bussola) per un indice di trackpoint ---
    @staticmethod
    def _bearing_deg(a, b):
        import math
        def _xy(p):
            lat = next((p[k] for k in ("lat_trackpoint","lat","latitude","y") if k in p), None)
            lon = next((p[k] for k in ("lon_trackpoint","lon","lng","longitude","x") if k in p), None)
            if lat is None or lon is None:
                raise ValueError("lat/lon mancanti nel punto")
            return float(lat), float(lon)

        lat1, lon1 = _xy(a); lat2, lon2 = _xy(b)
        lat1, lon1 = math.radians(lat1), math.radians(lon1)
        lat2, lon2 = math.radians(lat2), math.radians(lon2)
        dlon = lon2 - lon1
        y = math.sin(dlon) * math.cos(lat2)
        x = math.cos(lat1)*math.sin(lat2) - math.sin(lat1)*math.cos(lat2)*math.cos(dlon)
        brg = math.degrees(math.atan2(y, x))
        return (brg + 360.0) % 360.0   # 0=N, 90=E

    def _bearing_in_out(self, idx):
        pts = getattr(self, "database_traccia", [])
        n = len(pts)
        if n < 2 or idx < 0 or idx >= n:
            return None, None

        if 0 < idx < n-1:
            b_in  = self._bearing_deg(pts[idx-1], pts[idx])
            b_out = self._bearing_deg(pts[idx], pts[idx+1])
        else:
            # primo o ultimo
            b_in, b_out = 180.0, 0.0   # IN giù, OUT su (coerente con #1 senza offset)
        return b_in, b_out


    #                                                                                                     RICOSTRUISCE LE NOTE E PORTA LA NOTA DEL PUNTO EVIDENZIATO
    def _refresh_notes(self, scroll_to_idx=None):
        """Ricostruisce le note e, se indicato, scrolla/evidenzia la nota del trackpoint idx."""
        self.rebuild_notes_from_db()

        if scroll_to_idx is not None:
            # idx trackpoint -> num nota
            num = next((n for n, ix in getattr(self, "_note_idx_by_num", {}).items()
                        if ix == scroll_to_idx), None)
            if num is not None:
                self._ensure_note_visible(num, highlight=True)
                return 
            
        # fallback: evidenzia/scroll in base alla selezione corrente in mappa
        self.highlight_note_by_selected_point()



    #                                                                                                CENTRA IL WAYPOINT ASSOCIATO ALLA NOTA SE CLICCHI SULLA COLONNA A
    # --- Click su colonna A: centra waypoint associato
    def on_click_column_A(self, note_num: int):
        """
        Centra in mappa il waypoint associato alla nota e sincronizza la selezione
        (selected-point) così i comandi della toolbar leggono l'indice giusto.
        """
        idx = self._note_idx_by_num.get(note_num)
        if idx is None:
            return

        js = f"""
        (()=>{{
        try {{
            const line = map.getSource('track-line')?._data?.geometry?.coordinates;
            const c = Array.isArray(line) && line[{idx}] ? line[{idx}] : null;
            if (c) map.easeTo({{ center: c, duration: 400, essential: true }});

            // --- sync selected-point con la feature corrispondente ---
            const pts = map.getSource('track-points')?._data?.features;
            const sel = map.getSource('selected-point');
            if (Array.isArray(pts) && pts[{idx}] && sel) {{
            sel.setData({{ type: 'FeatureCollection', features: [ pts[{idx}] ] }});
            }}
        }} catch(e) {{}}
        }})();
        """

        page_holder = getattr(self, "web", None) or getattr(self, "webview", None) or getattr(self, "view", None)
        page = page_holder.page() if hasattr(page_holder, "page") else page_holder
        if page:
            page.runJavaScript(js, 0)

    def _clear_note_selection(self):
        for w in getattr(self, "_note_widgets_by_num", {}).values():
            try: w.set_selected(False)
            except Exception: pass

    def _ensure_note_visible(self, num, highlight=True):
        sa = getattr(self, "note_scroll", None)       # QScrollArea
        if not sa:
            return
        w = getattr(self, "_note_widgets_by_num", {}).get(num)
        if not w:
            return

        if highlight:
            self._clear_note_selection()
            try:
                w.set_selected(True)
            except Exception:
                pass

        # Defer a dopo il layout, così abbiamo geometrie corrette
        def _do_scroll():
            content = sa.widget()
            if not content:
                return
            try:
                y = w.mapTo(content, QPoint(0, 0)).y()
                vp_h = sa.viewport().height()
                bar = sa.verticalScrollBar()
                # centriamo la riga nel viewport
                target = int(y + w.height()/2 - vp_h/2)
                target = max(0, min(target, bar.maximum()))
                bar.setValue(target)
            except Exception:
                # fallback: top-align con piccolo margine
                try:
                    bar = sa.verticalScrollBar()
                    bar.setValue(max(0, y - 12))
                except Exception:
                    pass

        QTimer.singleShot(0, _do_scroll)

    def highlight_note_by_selected_point(self):
        """Porta in vista la nota associata alla selezione mappa
        SOLO se la nota è cambiata."""
        def _apply(i_sel):
            if i_sel is None:
                return
            try:
                idx = int(i_sel)
            except Exception:
                return

            # idx trackpoint -> num nota
            num = None
            for n, ix in getattr(self, "_note_idx_by_num", {}).items():
                if ix == idx:
                    num = n
                    break
            if num is None:
                return

            # centra SOLO se cambia
            if num != getattr(self, "_current_note_num", None):
                self._current_note_num = num
                self._ensure_note_visible(num, highlight=True)

        self._get_selected_index_from_map(_apply)


                      
    #                                                                                       HELPER PROPAGAZIONE A CATENA
    def _waypoint_indices(self):
        return [i for i,p in enumerate(self.database_traccia) if p.get("waypoint_nota") is not None]

    def _propagate_in_from(self, start_wp_idx):
        """Allinea road_type_in dei waypoint successivi = road_type_out del precedente."""
        wps = self._waypoint_indices()
        if not wps: return
        # parti dal waypoint 'start_wp_idx' (incluso) → propaga in avanti
        try:
            k = wps.index(start_wp_idx)
        except ValueError:
            # se non è un waypoint, parti dal primo successivo
            wps = [i for i in wps if i > start_wp_idx]
            if not wps: return
            k = 0
        for a, b in zip(wps[k:], wps[k+1:]):
            self.database_traccia[b]["road_type_in"] = int(self.database_traccia[a].get("road_type_out", 0) or 0)

    def _refresh_rows_from_idx(self, start_idx):
        """Aggiorna visualmente tutte le note da start_idx in poi (solo se esistono)."""
        for num, idx in self._note_idx_by_num.items():
            if idx >= start_idx:
                row = self._note_widgets_by_num.get(num)
                if row and hasattr(row, "canvasB"):
                    b_in, b_out = self._bearing_in_out(idx)
                    p = self.database_traccia[idx]
                    chosen = getattr(self, 'current_pen_style', None) or road_types[0]
                    def _rt(i):
                        try:
                            i = int(i);  return road_types[i] if 0 <= i < len(road_types) else chosen
                        except Exception:
                            return chosen
                    row.canvasB.set_bearings(b_in, b_out, _rt(p.get("road_type_in")), _rt(p.get("road_type_out")))
                    # step 1: aggiorna anche il bivio (se presente)
                    row.canvasB.set_bivio(p.get("bivio"))                    

    #                                                                     UTILITY PER CONTROLLARE SE I CAMPI DATABASE_TRACCIA VENGANO RIORGANIZZATI E MEMORIZZATI BENE  CTRL+5      
        
    def mostra_database_traccia(self):
        if hasattr(self, "database_traccia"):
            print("Contenuto di database_traccia:")
            for i, punto in enumerate(self.database_traccia):
                print(f"{i}: {punto}")
        else:
            print("database_traccia non definito.")

    #                                                                                                AGGIONRA BARRA DI STATO            
    # aggiorna barra di stato
    def aggiorna_status(self, azione: str):
        nome  = getattr(self, "nome_file_gpx", "") or "—"
        punti = len(getattr(self, "database_traccia", []))
        # messaggio persistente (timeout 0)
        self.statusBar().showMessage(f"{nome} — {punti} punti — {azione}", 0)

    #                                                                                             TOOLBAR ORIZZONTALE CON BOTTONI

    # --- Toolbar: lettura config + creazione pulsanti (tutti momentanei) ---
    def _make_toolbar_from_config(self, layout):
        cfg = self._load_buttons_config()

        # Ordine richiesto (fallback se nel JSON non c’è posizione/ordine)
        default_order = [
            "porta_sotto_push_button",
            "delete_push_button",
            "highway_road_type",
            "asphalt_road_type",
            "dirt_track_road_type",
            "off_road_type",
            "bivio_push_button",
            "cap_push_button",
            "text_push_button",
        ]

        # Determina le chiavi in ordine
        keys = []
        if isinstance(cfg, dict) and cfg:
            # Se il file ha position/order li uso, altrimenti il fallback
            barra = [(k, v) for k, v in cfg.items() if v.get("position", "barra") == "barra"]
            if any("order" in v for _, v in barra):
                barra.sort(key=lambda kv: kv[1].get("order", 1_000_000))
                keys = [k for k, _ in barra]
            else:
                keys = [k for k in default_order if k in cfg]
        else:
            keys = default_order
            cfg = {}

        self.bar_buttons = {}
        # Icon size: altezza = H_BAR-6, larghezza ampia (regge rettangolari)
        icon_w = max(int(self.H_BAR * 2.2), 60)
        icon_h = max(self.H_BAR - 6, 24)
        icon_size = QSize(icon_w, icon_h)

        TOOLTIPS_FALLBACK = {
            "porta_sotto_push_button": "Flip H",
            "delete_push_button":      "Elimina selezionato",
            "highway_road_type":       "Autostrada",
            "asphalt_road_type":       "Asfalto",
            "dirt_track_road_type":    "Pista",
            "off_road_type":           "Fuoripista",
            "bivio_push_button":       "Aggiungi bivio",
            "cap_push_button":         "Inserisci CAP",
            "text_push_button":        "Inserisci testo",
        }

        for key in keys:
            info = cfg.get(key, {})
            off_rel = info.get("off") or f"Icons/BOTTONI/{key}_off.png"
            on_rel  = info.get("on")  or f"Icons/BOTTONI/{key}_on.png"
            tip     = info.get("tooltip") or TOOLTIPS_FALLBACK.get(key) or key.replace("_", " ")

            off_path = self._resolve_path(off_rel)
            on_path  = self._resolve_path(on_rel)

            btn = QToolButton()
            btn.setAutoRaise(True)
            btn.setToolTip(tip)
            btn.setFixedHeight(self.H_BAR)
            btn.setIconSize(icon_size)
            btn.setIcon(QIcon(off_path))
            btn.setStyleSheet("QToolButton { border:0; padding:0 4px; }")

            # “Campanello”: on durante press, off al release + azione
            btn.pressed.connect(lambda op=on_path, b=btn: b.setIcon(QIcon(op)))
            btn.released.connect(lambda kp=key, offp=off_path, b=btn: (b.setIcon(QIcon(offp)), self._on_toolbar_click(kp)))

            layout.addWidget(btn)
            self.bar_buttons[key] = btn

    def _on_toolbar_click(self, key: str):
        # etichette (solo per status)
        labels = {
            "highway_road_type": "Tipo strada: Autostrada",
            "asphalt_road_type": "Tipo strada: Asfalto",
            "dirt_track_road_type": "Tipo strada: Pista",
            "off_road_type": "Tipo strada: Off-road",
            "bivio_push_button": "Aggiungi bivio",
        }

        # --- FLIP H: ribalta orizzontalmente l'icona selezionata ---
        if key == "porta_sotto_push_button":
            # cerca la riga che ha un'icona selezionata
            for num, row in self._note_widgets_by_num.items():
                cvs = getattr(row, "canvasB", None)
                if not cvs:
                    continue
                i = getattr(cvs, "icon_selected_idx", None)
                icons = getattr(cvs, "icons", None)

                if isinstance(icons, list) and isinstance(i, int) and 0 <= i < len(icons):
                    ic = icons[i]
                    ic["flipX"] = not bool(ic.get("flipX", False))  # toggle
                    # sync DB
                    idx = self._note_idx_by_num.get(num)
                    if idx is not None:
                        self.database_traccia[idx]["icone"] = list(icons)
                    cvs.update()
                    self.file_modificato_flag = True
                    self.statusBar().showMessage(self.t("status.icon_flipped"), 1200)
                    return

            # nessuna icona selezionata
            self.statusBar().showMessage(self.t("status.select_icon_first"), 1500)
            return

        # --- ELIMINA SELEZIONATO: icona se c'è, altrimenti bivio ---
        if key == "delete_push_button":
            # 1) ICONA
            for num, row in self._note_widgets_by_num.items():
                cvs = getattr(row, "canvasB", None)
                if not cvs: 
                    continue
                i = getattr(cvs, "icon_selected_idx", None)
                if isinstance(getattr(cvs, "icons", None), list) and i is not None and 0 <= i < len(cvs.icons):
                    cvs.icons.pop(i)
                    cvs.icon_selected_idx = None
                    cvs.update()
                    idx = self._note_idx_by_num.get(num)
                    if idx is not None:
                        self.database_traccia[idx]["icone"] = list(cvs.icons)
                    self.file_modificato_flag = True
                    self.statusBar().showMessage(self.t("status.icon_deleted"), 1200)
                    return

            # 2) BIVIO (elimina SOLO quello selezionato)
            for num, row in self._note_widgets_by_num.items():
                cvs = getattr(row, "canvasB", None)
                if not cvs or not getattr(cvs, "bivio_selected", False):
                    continue

                # usa il valore RAW per capire se è lista o singolo dict
                bl_raw = getattr(cvs, "bivi_lista", None)

                # --- caso LISTA ---
                if isinstance(bl_raw, list):
                    bl  = list(bl_raw)
                    sel = getattr(cvs, "bivio", None)

                    # trova indice robusto
                    bi = getattr(cvs, "bivio_selected_idx", None)
                    if not isinstance(bi, int) or not (0 <= bi < len(bl)) or bl[bi] is not sel:
                        bi = next((i for i, it in enumerate(bl) if it is sel), None)
                        if bi is None and isinstance(sel, dict):
                            spv, spt = sel.get("pivot"), sel.get("punta")
                            bi = next((i for i, it in enumerate(bl)
                                    if it.get("pivot") == spv and it.get("punta") == spt), None)
                    if not isinstance(bi, int):
                        self.statusBar().showMessage("Nessun bivio valido selezionato.", 1500)
                        return

                    # nuova lista senza il selezionato
                    new_bl = bl[:bi] + bl[bi+1:]

                    # sync DB
                    idx = self._note_idx_by_num.get(num)
                    if idx is not None:
                        if new_bl:
                            self.database_traccia[idx]["bivio"] = list(new_bl)
                        else:
                            self.database_traccia[idx].pop("bivio", None)

                    # refresh canvas (NO set_bivio(dict))
                    if new_bl:
                        new_idx = min(bi, len(new_bl) - 1)
                        cvs.set_bivi_lista(list(new_bl))
                        cvs.bivio = new_bl[new_idx]
                        cvs.bivio_selected_idx = new_idx
                        cvs.set_bivio_selected(True)
                    else:
                        cvs.set_bivi_lista([])
                        cvs.bivio = None
                        cvs.bivio_selected_idx = None
                        cvs.set_bivio_selected(False)

                    cvs.update()
                    self.file_modificato_flag = True
                    self.statusBar().showMessage("Bivio eliminato.", 1200)
                    return

                # --- caso SINGOLO DICT ---
                sel = getattr(cvs, "bivio", None)
                if isinstance(sel, dict):
                    idx = self._note_idx_by_num.get(num)
                    if idx is not None:
                        self.database_traccia[idx].pop("bivio", None)
                    cvs.bivio = None
                    cvs.set_bivio_selected(False)
                    cvs.set_bivi_lista([])   # allinea lo stato
                    cvs.update()
                    self.file_modificato_flag = True
                    self.statusBar().showMessage("Bivio eliminato.", 1200)
                    return
                
        if key == "cap_push_button":
            note_num = getattr(self, "_current_note_num", None) or getattr(self, "_recent_note_num", None)
            if not note_num:
                self.aggiorna_status("Seleziona una nota (colonna A)."); return
            idx = self._note_idx_by_num.get(note_num)
            if idx is None:
                self.aggiorna_status("Nota non trovata."); return

            next_wp = self._find_next_wp_idx(idx)  # waypoint successivo (non trackpoint)
            if next_wp is None:
                self.aggiorna_status("Nessun waypoint successivo."); return

            p = self.database_traccia[idx]
            row = self._note_widgets_by_num.get(note_num)
            if not row:
                return

            # toggle CAP rosso
            cap_vis = bool(row.cap_hdr.isVisible())
            if not cap_vis:
                try:
                    # bearing tra i due WAYPOINT
                    brg = float(self._bearing_deg(self.database_traccia[idx], self.database_traccia[next_wp]))

                    # distanza in km (haversine) tra i due WAYPOINT
                    R = 6371.0
                    a = self.database_traccia[idx]
                    b = self.database_traccia[next_wp]
                    lat1 = math.radians(float(a["lat_trackpoint"])); lon1 = math.radians(float(a["lon_trackpoint"]))
                    lat2 = math.radians(float(b["lat_trackpoint"])); lon2 = math.radians(float(b["lon_trackpoint"]))
                    dφ = lat2 - lat1
                    dλ = lon2 - lon1
                    s = math.sin(dφ/2)**2 + math.cos(lat1)*math.cos(lat2)*math.sin(dλ/2)**2
                    km = 2 * R * math.asin(math.sqrt(s))

                    # salva per persistenza
                    p["cap_hdr"] = brg
                    p["cap_km"]  = km
                    p["hide_cap_btm"] = True

                    # aggiorna UI
                    row.cap_hdr.setText(f"CAP {round(brg):.0f}º  km {km:.2f}")
                    row.cap_hdr.setVisible(True)
                    if hasattr(row, "cap_small"):
                        row.cap_small.hide()

                except Exception:
                    self.aggiorna_status(self.t("error.cap"))
            else:
                # spegni CAP rosso e ripristina piccolo
                p["cap_hdr"] = None
                p["cap_km"]  = None
                p["hide_cap_btm"] = False

                row.cap_hdr.clear()
                row.cap_hdr.setVisible(False)
                if hasattr(row, "cap_small"):
                    row.cap_small.show()

            self.file_modificato_flag = True
            return

        if key == "text_push_button":
            note_num = getattr(self, "_current_note_num", None) or getattr(self, "_recent_note_num", None)
            if not note_num:
                self.aggiorna_status("Seleziona una nota (colonna A)."); return
            self._start_edit_text(note_num)   # cursore dentro al campo, sblocca l'editor
            return

        # --- TIPI STRADA: prima BIVIO se selezionato, altrimenti OUT ---
        map_key_to_type = {
            "highway_road_type": 1,
            "asphalt_road_type": 2,
            "dirt_track_road_type": 3,
            "off_road_type": 4,
        }
        if key in map_key_to_type:
            t = map_key_to_type[key]
            th = float(road_types[t].get("thickness", 2.0))

            # 1) Se c'è un BIVIO selezionato → cambia solo lo spessore
            sel_note = None
            sel_canvas = None
            for num, row in self._note_widgets_by_num.items():
                if hasattr(row, "canvasB") and getattr(row.canvasB, "bivio_selected", False):
                    sel_note = num
                    sel_canvas = row.canvasB
                    break

            if sel_canvas is not None:
                bl = getattr(sel_canvas, "bivi_lista", None)
                sel = getattr(sel_canvas, "bivio", None)

                # indice robusto: trova l'oggetto selezionato nella lista
                bi = None
                if isinstance(bl, list) and isinstance(sel, dict):
                    for i, it in enumerate(bl):
                        if it is sel:
                            bi = i
                            break
                if bi is None:
                    bi = getattr(sel_canvas, "bivio_selected_idx", getattr(sel_canvas, "bivio_idx", None))

                try:
                    if isinstance(sel, dict):
                        sel["th"] = th
                        sel["rt"] = t
                    if isinstance(bi, int) and isinstance(bl, list) and 0 <= bi < len(bl):
                        bl[bi]["th"] = th
                        bl[bi]["rt"] = t
                    sel_canvas.update()
                except Exception:
                    pass
                # sincronizza DB usando bi/bl già definiti
                idx = self._note_idx_by_num.get(sel_note)
                if idx is not None:
                    cur = self.database_traccia[idx].get("bivio")
                    if isinstance(cur, list):
                        if isinstance(bi, int) and 0 <= bi < len(cur):
                            cur[bi]["th"] = th
                            cur[bi]["rt"] = t
                        self.database_traccia[idx]["bivio"] = cur
                    elif isinstance(cur, dict):
                        cur["th"] = th
                        cur["rt"] = t
                        self.database_traccia[idx]["bivio"] = cur
                    self.file_modificato_flag = True

                self.statusBar().showMessage(f"Spessore bivio: {road_types[t].get('name','')}", 1200)
                return

            # 2) Altrimenti: applica il tipo al VETTORE OUT selezionato
            try:
                self.current_pen_style = road_types[t]
            except Exception:
                self.current_pen_style = road_types[0]
            self.apply_road_type_out(self.current_pen_style)
            self.aggiorna_status({
                "highway_road_type": "Tipo strada: Autostrada",
                "asphalt_road_type": "Tipo strada: Asfalto",
                "dirt_track_road_type": "Tipo strada: Pista",
                "off_road_type": "Tipo strada: Off-road",
            }.get(key, key))
            return

        # --- STEP 1: Aggiungi un BIVIO statico (solo se OUT NON è selezionato) ---
        if key == "bivio_push_button":
            # OUT non deve essere selezionato
            if getattr(self, "_selected_vector_num", None) is not None:
                self.aggiorna_status("Deseleziona il vettore OUT (clic vuoto nel canvas) prima di aggiungere un bivio.")
                return

            # serve una nota corrente (gialla)
            note_num = getattr(self, "_current_note_num", None) or getattr(self, "_recent_note_num", None)
            if not note_num:
                self.aggiorna_status("Seleziona una nota (colonna A) prima di aggiungere il bivio.")
                return

            idx = self._note_idx_by_num.get(note_num)
            if idx is None:
                self.aggiorna_status("Nota non trovata.")
                return

            # calcola Δ = b_out - b_in per allineare il bivio
            b_in, b_out = self._bearing_in_out(idx)
            if b_in is None or b_out is None:
                self.aggiorna_status("Impossibile calcolare il Δ OUT per questa nota.")
                return

            row = self._note_widgets_by_num.get(note_num)
            if not row or not hasattr(row, "canvasB"):
                self.aggiorna_status("Canvas della nota non disponibile.")
                return

            # lunghezza in funzione del canvas
            w = max(1, row.canvasB.width()); h = max(1, row.canvasB.height())
            L = max(22, min(w, h) * 0.36)

            def norm180(a):
                while a > 180: a -= 360
                while a <= -180: a += 360
                return a

            d = norm180(float(b_out) - float(b_in) + 45.0)  # +45°
            ang = math.radians(d)
            ux, uy = math.sin(ang), -math.cos(ang)

            L2 = L * 0.9
            dx, dy = ux * L2, uy * L2

            # salva in lista (append, non sovrascrivere)
            cur = self.database_traccia[idx].get("bivio")
            if isinstance(cur, list):
                lst = cur
            elif isinstance(cur, dict):
                lst = [cur]
            else:
                lst = []

            new_biv = {"pivot": (0.0, 0.0), "punta": (dx, dy), "th": 2.0, "rt": 3}
            lst.append(new_biv)
            self.database_traccia[idx]["bivio"] = lst
            row.canvasB.set_bivi_lista(lst)        # mantieni tutta la lista
            row.canvasB.bivio = lst[-1]            # seleziona l’ultimo (dict)
            row.canvasB.bivio_selected_idx = len(lst) - 1
            row.canvasB.set_bivio_selected(True)
            row.canvasB.update()
            self._selected_vector_num = None
            row.canvasB.set_selected(False)


            self.file_modificato_flag = True
            self.aggiorna_status("Bivio aggiunto")
            return
        
        # fallback: solo status
        self.aggiorna_status(labels.get(key, key))

    #                                                                                    SALVA IN DATABASE_TRACCIA I DATI SE IL BIVIO È STATO CAMBIATO
    def on_bivio_changed(self, num, bivio_dict):
        """Salva il bivio modificato SENZA sovrascrivere gli altri bivi (fix: non usare sempre l'ultimo)."""
        idx = self._note_idx_by_num.get(num)
        if idx is None:
            return

        row = self._note_widgets_by_num.get(num)
        cvs = getattr(row, "canvasB", None) if row else None

        cur = self.database_traccia[idx].get("bivio")

        if isinstance(cur, list):
            lst = cur
        elif isinstance(cur, dict):
            lst = [cur]
        else:
            lst = []

        # retro-compat: se non abbiamo lista, salva come dict singolo
        if not lst:
            if isinstance(bivio_dict, dict):
                self.database_traccia[idx]["bivio"] = dict(bivio_dict)
            if cvs:
                cvs.set_bivio(self.database_traccia[idx].get("bivio"))
                cvs.update()
            self.file_modificato_flag = True
            return

        sel = getattr(cvs, "bivio", None) if cvs else None
        bi  = getattr(cvs, "bivio_selected_idx", None) if cvs else None

        # indice robusto
        if not (isinstance(bi, int) and 0 <= bi < len(lst)) or (isinstance(sel, dict) and lst[bi] is not sel):
            if isinstance(sel, dict):
                bi = next((i for i, it in enumerate(lst) if it is sel), None)
                if bi is None:
                    spv, spt = sel.get("pivot"), sel.get("punta")
                    bi = next((i for i, it in enumerate(lst)
                               if it.get("pivot") == spv and it.get("punta") == spt), 0)
            else:
                bi = 0

        # aggiorna IN PLACE (non sostituisce altri dict)
        if isinstance(bivio_dict, dict):
            lst[int(bi)].update(bivio_dict)

        self.database_traccia[idx]["bivio"] = lst

        # riallinea canvas e mantieni selezione
        if cvs:
            cvs.set_bivi_lista(lst)
            cvs.bivio = lst[int(bi)]
            cvs.bivio_selected_idx = int(bi)
            cvs.set_bivio_selected(True)
            cvs.update()

        self.file_modificato_flag = True

    # Percorsi relativi: cerca le immagini in ./Icons e ./Icons/BOTTONI rispetto a main.py

    def _resolve_path(self, relpath: str) -> str:
        base = os.path.dirname(__file__)
        return os.path.join(base, relpath)

    def _load_buttons_config(self):
        """Carica la config della barra SOLO da ./toolbar_buttons.json"""
        try:
            base = os.path.dirname(__file__)
            p = os.path.join(base, "toolbar_buttons.json")
            with open(p, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            return {}

        labels = {
            "porta_sotto_push_button": "Porta sotto (demo)",
            "delete_push_button": "Elimina oggetto (demo)",
            "highway_road_type": "Tipo strada: Autostrada",
            "asphalt_road_type": "Tipo strada: Asfalto",
            "dirt_track_road_type": "Tipo strada: Pista",
            "off_road_type": "Tipo strada: Off-road",
            "bivio_push_button": "Aggiungi bivio",  # <--- NEW
            "cap_push_button": "Inserisci CAP (demo)",
            "text_push_button": "Inserisci testo (demo)",
        }


        # mappa tasto -> indice in road_types
        map_tipo = {
            "highway_road_type": 1,
            "asphalt_road_type": 2,
            "dirt_track_road_type": 3,
            "off_road_type": 4,
        }

        # se è un bottone tipo-strada e c'è una nota selezionata → applica
        if key in map_tipo:
            if self._selected_vector_num is None:
                self.aggiorna_status("Seleziona un vettore OUT (clic in colonna B) prima di scegliere il tipo strada")
                return
            tipo = map_tipo[key]
            self.apply_road_type_out(road_types[tipo])
            self.file_modificato_flag = True
            self.aggiorna_status(labels.get(key, key))
            return

        # --- NEW: aggiungi un bivio statico sulla nota corrente (solo se OUT non selezionato)
        if key == "bivio_push_button":
            # vincoli step 1: nota gialla (corrente) esiste e OUT non selezionato
            if getattr(self, "_selected_vector_num", None) is not None:
                self.aggiorna_status("Deseleziona il vettore OUT prima di aggiungere un bivio (clic vuoto nel canvas).")
                return
            note_num = getattr(self, "_current_note_num", None)
            if note_num is None:
                self.aggiorna_status("Seleziona una nota (clic sulla colonna A) prima di aggiungere un bivio.")
                return

            idx = self._note_idx_by_num.get(note_num)
            if idx is None:
                self.aggiorna_status("Nota non trovata.")
                return

            # prendi bearings per allineare il bivio al Δ OUT
            b_in, b_out = self._bearing_in_out(idx)
            if b_in is None or b_out is None:
                self.aggiorna_status("Impossibile calcolare il Δ OUT per questa nota.")
                return

            # trova il canvas della riga corrente per dimensionare la lunghezza
            row = self._note_widgets_by_num.get(note_num)
            if not row or not hasattr(row, "canvasB"):
                self.aggiorna_status("Canvas della nota non disponibile.")
                return
            w = max(1, row.canvasB.width()); h = max(1, row.canvasB.height())
            L = max(22, min(w, h) * 0.36)
            # direzione = Δ = b_out - b_in (normalizzato)
            def norm180(a):
                while a > 180: a -= 360
                while a <= -180: a += 360
                return a
            d = norm180(float(b_out) - float(b_in))
            ang = math.radians(d)
            ux, uy = math.sin(ang), -math.cos(ang)  # 0° = su

            L2 = L * 0.9  # lunghezza bivio
            dx, dy = ux * L2, uy * L2

            # salva nel database: coordinate relative al centro canvas (come i vettori)
            cur = self.database_traccia[idx].get("bivio")
            if isinstance(cur, list):
                lst = cur
            elif isinstance(cur, dict):
                lst = [cur]
            else:
                lst = []

            new_biv = {"pivot": (0.0, 0.0), "punta": (dx, dy), "th": 2.0, "rt": 0}
            lst.append(new_biv)
            self.database_traccia[idx]["bivio"] = lst

            # mostra l’ultimo (la tua set_bivio accetta lista)
            row.canvasB.set_bivio(lst)
            row.canvasB.update()

            row.canvasB.update()

            self.file_modificato_flag = True
            self.aggiorna_status("Bivio aggiunto")
            return

        # altrimenti: solo feedback
        self.aggiorna_status(labels.get(key, key))




    # Seleziona file tipo "<prefisso><num>_<nome>.png" (es. "t03_bosco.png"), ordina per numero.
    # Icon-only: altezza fissa TAB_ICON_H, larghezza fino a TAB_ICON_W_MAX (mantiene proporzioni).
    # Click: solo feedback in status bar (nessuna azione). Lambda con *args per compatibilità clicked()/clicked(bool).

    def _load_tab_icons(self, nome_tab):
        print("LOAD TAB:", nome_tab)
        if nome_tab in getattr(self, "_tabs_loaded", set()):
            return
        w, flow, prefix = self._tab_map[nome_tab]

        base_app = Path(getattr(sys, "_MEIPASS", os.getcwd()))
        # cerca in 3 posti: USER, Icons, _internal/Icons
        search_dirs = [
            USER_ICONS,
            base_app / "Icons",
            base_app / "_internal" / "Icons",
        ]

        patt = re.compile(rf"^{prefix}(\d+)[-_]?(.*)\.(png|jpg|jpeg|webp)$", re.IGNORECASE)
        seen = set()
        files = []

        for d in search_dirs:
            if not d or not d.exists():
                continue
            for fn in os.listdir(d):
                m = patt.match(fn)
                if not m:
                    continue
                # dedup per filename (l’USER vince perché viene prima)
                if fn.lower() in seen:
                    continue
                seen.add(fn.lower())

                num = int(m.group(1))
                label = m.group(2).replace("_", " ").strip()
                files.append((num, label, str(d / fn)))  # path completo

        files.sort(key=lambda x: (x[0], x[1].lower()))

        H = self.TAB_ICON_H
        WMAX = self.TAB_ICON_W_MAX

        for num, label, full in files:
            pix = QPixmap(full)
            if pix.isNull():
                continue
            w_nat, h_nat = pix.width(), pix.height()
            scale = H / max(1, h_nat)
            w_scaled = int(round(w_nat * scale))
            w_scaled = max(24, min(WMAX, w_scaled))

            btn = QToolButton()
            btn.setAutoRaise(True)
            tip = f"{prefix.upper()}{num:02d} · {label}" if label else f"{prefix.upper()}{num:02d}"
            btn.setToolTip(tip)
            btn.setIcon(QIcon(pix))
            btn.setIconSize(QSize(w_scaled, H))
            btn.setFixedSize(w_scaled + 4, H + 4)
            btn.setStyleSheet("QToolButton { border: 0; padding: 0 2px; }")
            btn.clicked.connect(lambda *_, b=btn, n=nome_tab: self._select_tab_icon(n, b))
            btn.clicked.connect(lambda _=False, b=btn, tab=nome_tab: self._on_tab_icon_click(tab, b))
            flow.addWidget(btn)

            # registra mapping, includendo anche 'name'
            self._tab_btn_info.setdefault(nome_tab, {})[btn] = {
                "file": full,
                "name": os.path.basename(full),
                "num": num,
                "label": label
            }

        self._tabs_loaded.add(nome_tab)

    #                                                                                      AGGIUNGE ICONA ALLA COLONNA B
    def _on_tab_icon_click(self, tab_name, btn):
        info = self._tab_btn_info.get(tab_name, {}).get(btn)
        if not info:
            self.statusBar().showMessage("Icona non trovata.", 1500)
            return

        icon_name = info.get("name") or os.path.basename(info.get("file", ""))
        nota = getattr(self, "nota_corrente", None)
        if not nota and hasattr(self, "get_nota_selezionata"):
            nota = self.get_nota_selezionata()
        if nota and icon_name:
            nota.icon_name = icon_name
            path = resolve_icon_path(icon_name)
            if path:
                nota.set_icon_pixmap(path)

        # nota corrente
        note_num = getattr(self, "_current_note_num", None) or getattr(self, "_recent_note_num", None)
        if not note_num:
            self.statusBar().showMessage("Seleziona una nota (colonna A) prima di aggiungere un'icona.", 2000)
            return
        idx = self._note_idx_by_num.get(note_num)
        if idx is None:
            self.statusBar().showMessage("Nota non trovata.", 1500)
            return

        # append su database_traccia[idx]["icone"]
        ic = {"file": info.get("file"), "angle": 0.0, "size": 28, "flipX": False}  # pos assente ⇒ alto-sinistra
        cur = self.database_traccia[idx].get("icone")
        lst = cur if isinstance(cur, list) else []
        lst.append(ic)
        self.database_traccia[idx]["icone"] = lst

        # refresh SOLO quella riga
        row = self._note_widgets_by_num.get(note_num)
        if row and hasattr(row, "canvasB"):
            row.canvasB.set_icons(lst)  # PASSO 1: già fatto
            row.canvasB.update()

        self.file_modificato_flag = True
        self.statusBar().showMessage(f"Aggiunta icona: {info.get('label','')}", 1200)

    def _start_edit_text(self, note_num: int):
        """Sblocca l'editor testo della nota e mette il cursore dentro."""
        row = self._note_widgets_by_num.get(note_num)
        if not row or not hasattr(row, "desc_edit"):
            return
        e = row.desc_edit
        # collega la nota all'editor (utile per il salvataggio su FocusOut)
        if not e.property("note_num"):
            e.setProperty("note_num", note_num)
        e.setReadOnly(False)
        e.setFocus()
        e.selectAll()
        # 🔒 chiudi l'editing quando il focus esce dall'editor (one-shot)
        app = QApplication.instance()
        def _on_focus_changed(old, new):
            if old is e and new is not e:
                self._commit_note_text(note_num)  # salva + torna read-only
                try:
                    app.focusChanged.disconnect(_on_focus_changed)
                except Exception:
                    pass
        app.focusChanged.connect(_on_focus_changed)

    def _commit_note_text(self, note_num: int):
        """Blocca l'editing, salva nel database, applica il limite."""
        row = self._note_widgets_by_num.get(note_num)
        if not row or not hasattr(row, "desc_edit"):
            return
        e = row.desc_edit

        # limite caratteri
        maxlen = int(getattr(self, "TEXT_MAX_LEN", 50))
        txt = (e.toPlainText() or "").replace("\n", " ")
        if len(txt) > maxlen:
            e.blockSignals(True)
            txt = txt[:maxlen]
            e.setPlainText(txt)
            e.blockSignals(False)

        # salva su DB
        idx = self._note_idx_by_num.get(note_num)
        if idx is not None:
            self.database_traccia[idx]["testo"] = txt

        # riblocca
        e.setReadOnly(True)
        e.clearFocus()
        self.file_modificato_flag = True

    def _fit_two_lines(self, editor):
        """Mantieni il testo dentro 2 righe visibili e sotto TEXT_MAX_LEN."""
        # limite caratteri (soft cap)
        maxlen = int(getattr(self, "TEXT_MAX_LEN", 50))
        txt = (editor.toPlainText() or "").replace("\n", " ")

        changed = False
        if len(txt) > maxlen:
            txt = txt[:maxlen]
            changed = True

        # taglia finché l'altezza del documento rientra nel viewport (2 righe)
        allowed = editor.viewport().height()

        def doc_h():
            doc = editor.document()
            doc.adjustSize()
            return int(doc.size().height())

        # applica e, se serve, riduci finché sta nel box
        editor.blockSignals(True)
        editor.setPlainText(txt)
        while txt and doc_h() > allowed:
            txt = txt[:-1]
            editor.setPlainText(txt)
        editor.blockSignals(False)


        # rimetti il cursore alla fine
        cur = editor.textCursor()
        cur.setPosition(len(txt))
        editor.setTextCursor(cur)

    def _save_icons_for_note(self, note_num, icons):
        idx = self._note_idx_by_num.get(note_num)
        if idx is None: return
        self.database_traccia[idx]["icone"] = list(icons) if isinstance(icons, list) else []
        self.file_modificato_flag = True

    def controlla_aggiornamenti(self):
        box = QMessageBox(self)
        box.setWindowTitle(self.t("updates.title"))
        box.setText(self.t("updates.text", ver=APP_VERSION))
        btn = box.addButton(self.t("updates.open_page"), QMessageBox.AcceptRole)
        box.addButton(self.t("updates.close"), QMessageBox.RejectRole)
        box.exec()
        if box.clickedButton() == btn:
            QDesktopServices.openUrl(QUrl("https://roadbook-system.com/roadbook-editor.html"))


#                                                                                                       --- FlowLayout (per i TAB icone) -------------------------------------------
# Layout "a flusso": dispone i widget in riga e va a capo quando lo spazio finisce.
# Usato nei tab TERRENO/AVVISI/INFO!/POI/SEGNALI per impaginare le icone.

class FlowLayout(QLayout):
    def __init__(self, parent=None, margin=0, hspacing=6, vspacing=6, margins=None):
        super().__init__(parent)
        self._items = []
        self._hspace = hspacing
        self._vspace = vspacing
        self.bivi_lista = None
        if margins is not None:
            l, t, r, b = margins
            self.setContentsMargins(l, t, r, b)
        else:
            self.setContentsMargins(margin, margin, margin, margin)

    def addItem(self, item): self._items.append(item)
    def count(self): return len(self._items)
    def itemAt(self, i): return self._items[i] if 0 <= i < len(self._items) else None
    def takeAt(self, i): return self._items.pop(i) if 0 <= i < len(self._items) else None
    def expandingDirections(self): return 0
    def hasHeightForWidth(self): return True
    def heightForWidth(self, w): return self._do_layout(QRect(0, 0, w, 0), True)
    def setGeometry(self, rect): super().setGeometry(rect); self._do_layout(rect, False)
    def sizeHint(self): return self.minimumSize()
    def minimumSize(self):
        s = QSize()
        for it in self._items:
            s = s.expandedTo(it.minimumSize())
        m = self.contentsMargins()
        s += QSize(m.left()+m.right(), m.top()+m.bottom())
        return s

    def _do_layout(self, rect, test_only):
        x = rect.x(); y = rect.y()
        line_height = 0
        m = self.contentsMargins()
        x += m.left(); y += m.top()
        effective_rect = QRect(rect.x()+m.left(), rect.y()+m.top(),
                               rect.width()-m.left()-m.right(),
                               rect.height()-m.top()-m.bottom())
        for it in self._items:
            w = it.sizeHint().width()
            h = it.sizeHint().height()
            if x + w > effective_rect.right() and line_height > 0:
                x = effective_rect.x()
                y = y + line_height + self._vspace
                line_height = 0
            if not test_only:
                it.setGeometry(QRect(QPoint(x, y), it.sizeHint()))
            x += w + self._hspace
            line_height = max(line_height, h)
        return y + line_height - rect.y() + m.bottom()
    
#                                                                                                              DISEGNO VETTORI DIREZIONE
class VectorCanvas(QFrame):
    """
    Disegna i vettori della nota:
    - IN: sempre verso il basso (6 o'clock)
    - OUT: angolo relativo Δ = (bearing_out - bearing_in) con freccia
    - cerchietto bianco al centro
    - BIVIO: rotazione dalla punta + traslazione con maniglia sulla linea
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.b_in = None
        self.b_out = None
        self.style_in  = {"color": "#dcdcdc", "thickness": 2.0}
        self.style_out = {"color": "#dcdcdc", "thickness": 2.0}
        self.setStyleSheet("background: transparent;")
        self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
        self.setCursor(Qt.PointingHandCursor)
        self.is_selected = False
        self.on_clicked = None   # selezione vettore OUT
        self.setObjectName("VectorCanvas")
        self.icons = []  # lista di dict: {"file": str, "pos": (x,y), "angle": 0.0, "size": 28}
        self.icon_selected_idx = None        # indice icona selezionata (o None)
        self._icon_rects_widget = []         # QRect per hit-test, in coord. widget, allineati alla lista icone
        self.on_icon_clicked = None          # callback opzionale (non la usiamo ora)
        self._icon_rot_rects_widget = []   # QRect della maniglia “rotate” per ogni icona (coord. widget)
        self._icon_rot_handles_widget = []  # (x,y) maniglia rotazione per ogni icona, in coord. widget
        self._icon_rotating = False
        self._icon_rot_pivot_w = (0.0, 0.0)     # pivot in coord. widget (TL dell’icona)
        self._icon_rot_start_mouse_angle = 0.0
        self._icon_rot_start_icon_angle  = 0.0
        self.setFocusPolicy(Qt.StrongFocus)

        # Bivi
        self.bivi_lista = None                 # <-- lista di dict, oppure None
        self.bivio = None                      # dict selezionato/mostrato
        self.bivio_selected = False
        self.on_bivio_clicked = None           # selezione bivio
        self.on_blank_clicked = None           # click vuoto
        self.on_bivio_changed = None           # callback salvataggio dict

        # Drag state
        self._drag_mode = None                 # "rotate" | "translate"
        self._bivio_len = 0.0                  # per rotazione a lunghezza fissa
        self._drag_handle_offset = (0.0, 0.0)
        self._handle_start = (0.0, 0.0)
        self._seg_start_pv = (0.0, 0.0)
        self._seg_start_pt = (0.0, 0.0)
        self._icon_drag_mode = None
        self._icon_drag_start_mouse = (0, 0)   # px,py in coordinate widget
        self._icon_drag_start_pos = (0.0, 0.0) # x,y in coordinate canvas (orig. al centro)
        self.on_icon_changed = None            # callback (la useremo nel passo 3)

    # ---- API usate dal resto dell'app
    def set_bearings(self, b_in, b_out, style_in=None, style_out=None):
        self.b_in = b_in
        self.b_out = b_out
        if isinstance(style_in, dict):  self.style_in  = style_in
        if isinstance(style_out, dict): self.style_out = style_out
        self.update()

    def set_selected(self, sel: bool):
        self.is_selected = bool(sel)
        self.update()

    def set_bivio_selected(self, sel: bool):
        self.bivio_selected = bool(sel)
        self.update()

    def set_bivi_lista(self, data):
        """Accetta lista o dict: memorizza tutti i bivi per il disegno."""
        if isinstance(data, list):
            self.bivi_lista = data
        elif isinstance(data, dict):
            self.bivi_lista = [data]
        else:
            self.bivi_lista = None
        self.update()

    def set_bivio(self, d):
        """Accetta dict o lista: tiene anche la lista completa per hit-test."""
        if isinstance(d, list):
            self.bivi_lista = d
            self.bivio = d[-1] if d else None
        elif isinstance(d, dict):
            self.bivi_lista = [d]
            self.bivio = d
        else:
            self.bivi_lista = None
            self.bivio = None
        self.update()

    # ---- Input
    def mousePressEvent(self, ev):
        self.setFocus()
        self._icon_dragging = False
        pos = ev.position() if hasattr(ev, "position") else ev.localPos()
        x = pos.x() - self.width()*0.5
        y = pos.y() - self.height()*0.5

        # --- SELEZIONE ICONA (ha priorità) ---
        if self._icon_rects_widget:
            # pos widget (non centrate)
            posw = ev.position() if hasattr(ev, "position") else ev.localPos()
            px, py = int(posw.x()), int(posw.y())
            # 0) Se ho già un'icona selezionata, prova PRIMA la sua maniglia rossa
            if self.icon_selected_idx is not None and \
            0 <= self.icon_selected_idx < len(self._icon_rot_rects_widget):
                rot_rect = self._icon_rot_rects_widget[self.icon_selected_idx]
                if rot_rect and rot_rect.contains(px, py):
                    # avvia ROTAZIONE della icona già selezionata
                    ic = self.icons[self.icon_selected_idx]
                    # assicurati che abbia una pos salvata (pivot = TL in coord. canvas)
                    if not isinstance(ic.get("pos"), (tuple, list)) or len(ic["pos"]) != 2:
                        rectw = self._icon_rects_widget[self.icon_selected_idx]
                        cx, cy = self.width()*0.5, self.height()*0.5
                        ic["pos"] = (rectw.x() - cx, rectw.y() - cy)

                    # calcola pivot in coord. widget
                    cx, cy = self.width()*0.5, self.height()*0.5
                    ix, iy = ic.get("pos", (0.0, 0.0))
                    pvx, pvy = cx + ix, cy + iy

                    # stato iniziale rotazione
                    self._icon_rotating = True
                    self._icon_rot_pivot_w = (pvx, pvy)
                    self._icon_rot_start_icon_angle  = float(ic.get("angle", 0.0) or 0.0)
                    self._icon_rot_start_mouse_angle = math.degrees(math.atan2(py - pvy, px - pvx))
                    self.setMouseTracking(True)
                    self.update()
                    return

            hit_idx = None
            # scorri al contrario: l'ultima disegnata in primo piano
            for i in range(len(self._icon_rects_widget)-1, -1, -1):
                rect = self._icon_rects_widget[i]
                if rect and rect.contains(px, py):
                    hit_idx = i
                    break

            if hit_idx is not None:
                self.icon_selected_idx = hit_idx

                # prepara il drag dell'icona selezionata
                ic = self.icons[self.icon_selected_idx] if isinstance(self.icons, list) else None
                if isinstance(ic, dict):
                    posw = ev.position() if hasattr(ev, "position") else ev.localPos()
                    px, py = float(posw.x()), float(posw.y())
                    cx, cy = self.width()*0.5, self.height()*0.5

                    # 1) assicura SEMPRE ic["pos"] PRIMA di calcolare l'offset
                    if not isinstance(ic.get("pos"), (tuple, list)) or len(ic["pos"]) != 2:
                        rectw = self._icon_rects_widget[self.icon_selected_idx]
                        ic["pos"] = (rectw.x() - cx, rectw.y() - cy)

                    ix, iy = ic["pos"]

                    # 2) ora l’offset è corretto → niente “salto”
                    self._icon_drag_offset_w = (px - (cx + ix), py - (cy + iy))
                    self._icon_dragging = True

                self.update()
                if callable(self.on_icon_clicked):
                    self.on_icon_clicked()
                # --- INIZIO DRAG ICONA SELEZIONATA ---
                ic = self.icons[hit_idx]

                # assicurati che l'icona abbia una posizione salvata
                if not isinstance(ic.get("pos"), (tuple, list)) or len(ic["pos"]) != 2:
                    # ricava il TL del rect salvato (coord. widget) e converti in coord. canvas (origine al centro)
                    rectw = self._icon_rects_widget[hit_idx]
                    cx, cy = self.width()*0.5, self.height()*0.5
                    ic["pos"] = (rectw.x() - cx, rectw.y() - cy)

                # prepara lo stato di drag
                self._icon_drag_mode = "icon-translate"
                self._icon_drag_start_mouse = (px, py)            # mouse in coord. widget
                self._icon_drag_start_pos = tuple(ic["pos"])      # pos icona in coord. canvas
                self.setMouseTracking(True)
                return
            
        # 0) Maniglia TRASLAZIONE (sulla linea, arretrata dai trattini)
        if isinstance(self.bivio, dict):
            pv = self.bivio.get("pivot"); pt = self.bivio.get("punta")
            if pv and pt:
                vx, vy = pt[0]-pv[0], pt[1]-pv[1]
                L = math.hypot(vx, vy)
                if L > 1e-6:
                    ux, uy = vx/L, vy/L
                    extra = 7.0
                    base  = max(18.0, min(L - 6.0, 0.35 * L))
                    offH  = min(L - 6.0, base + extra)
                    hx = pt[0] - ux * offH
                    hy = pt[1] - uy * offH
                    if (x - hx)**2 + (y - hy)**2 <= 9**2:
                        if not self.bivio_selected and callable(self.on_bivio_clicked):
                            self.on_bivio_clicked()
                        self._drag_mode = "translate"
                        self._drag_handle_offset = (x - hx, y - hy)
                        self._handle_start = (hx, hy)
                        self._seg_start_pv = (pv[0], pv[1])
                        self._seg_start_pt = (pt[0], pt[1])
                        self.setMouseTracking(True)
                        return

        # 1) Maniglia ROTAZIONE (punta)
        if isinstance(self.bivio, dict):
            pv = self.bivio.get("pivot"); pt = self.bivio.get("punta")
            if pv and pt:
                if (x-pt[0])**2 + (y-pt[1])**2 <= 14**2:
                    if not self.bivio_selected and callable(self.on_bivio_clicked):
                        self.on_bivio_clicked()
                    dx, dy = pt[0]-pv[0], pt[1]-pv[1]
                    self._bivio_len = math.hypot(dx, dy) or 1.0
                    self._drag_mode = "rotate"
                    self.setMouseTracking(True)
                    return

        # 2) Cerchietto centrale = selezione OUT (priorità)
        R_hit = 12.0
        if (x*x + y*y) <= (R_hit*R_hit):
            # NEW: deseleziona tutto il resto
            self.bivio_selected = False
            self.icon_selected_idx = None
            self.update()

            if callable(self.on_blank_clicked):
                self.on_blank_clicked()
            if callable(self.on_clicked):
                self.on_clicked()
            return

        # 3) Selezione BIVIO (priorità a quello già selezionato; poi il più vicino)
        # pool: tutti i bivi (ultimi creati per primi)
        items = self.bivi_lista if isinstance(self.bivi_lista, list) else None
        if isinstance(items, list):
            pool = list(reversed(items))        # priorità agli ultimi creati
        elif isinstance(self.bivio, dict):
            pool = [self.bivio]
        else:
            pool = []

        # --- PRIORITÀ: se clicco vicino al bivio già selezionato, NON cambiare selezione
        LOCK_R = 16  # px di isteresi sugli handle
        if isinstance(self.bivio, dict):
            pv0 = self.bivio.get("pivot"); pt0 = self.bivio.get("punta")
            if pv0 and pt0:
                if (x - pv0[0])**2 + (y - pv0[1])**2 <= LOCK_R**2 or (x - pt0[0])**2 + (y - pt0[1])**2 <= LOCK_R**2:
                    # mantieni selezionato l'attuale bivio
                    if callable(self.on_bivio_clicked):
                        self.on_bivio_clicked()
                    return

        # --- Scelta del candidato "migliore" (come prima)
        candidates = []
        for _b in pool:
            pv = _b.get("pivot"); pt = _b.get("punta")
            if not (pv and pt):
                continue
            dx, dy = pt[0] - pv[0], pt[1] - pv[1]
            L2 = dx*dx + dy*dy
            if L2 <= 1e-6:
                continue

            # proiezione del click sul segmento
            t  = ((x - pv[0]) * dx + (y - pv[1]) * dy) / L2
            tc = 0.0 if t < 0.0 else (1.0 if t > 1.0 else t)
            px, py = pv[0] + tc*dx, pv[1] + tc*dy

            # vicinanze (radii leggermente aumentati)
            near_center    = False  # (eventuale blocco centrale, qui disattivo)
            near_last_half = (0.5 <= t <= 1.2) and ((x - px)**2 + (y - py)**2 <= 12**2)
            near_tip       = ((x - pt[0])**2 + (y - pt[1])**2 <= 14**2)

            if (near_last_half or near_tip) and not near_center:
                dist2 = (x - px)**2 + (y - py)**2 if near_last_half else (x - pt[0])**2 + (y - pt[1])**2
                candidates.append((dist2, _b))

        if candidates:
            candidates.sort(key=lambda c: c[0])
            self.bivio = candidates[0][1]  # seleziona quel dict
            if callable(self.on_bivio_clicked):
                self.on_bivio_clicked()
            return

        # 4) --- click vuoto = deseleziona ---
        self.bivio_selected = False
        self.icon_selected_idx = None
        self.update()
        if callable(self.on_blank_clicked):
            self.on_blank_clicked()

    def mouseMoveEvent(self, ev):
        # --- ROTAZIONE ICONA (drag dal pallino rosso) ---
        if self._icon_rotating and self.icon_selected_idx is not None \
        and isinstance(self.icons, list) and 0 <= self.icon_selected_idx < len(self.icons):

            posw = ev.position() if hasattr(ev, "position") else ev.localPos()
            px, py = float(posw.x()), float(posw.y())

            pvx, pvy = self._icon_rot_pivot_w               # pivot in coord. widget
            cur_ang  = math.degrees(math.atan2(py - pvy, px - pvx))
            delta    = cur_ang - self._icon_rot_start_mouse_angle
            base     = float(self._icon_rot_start_icon_angle or 0.0)

            self.icons[self.icon_selected_idx]["angle"] = base + delta
            self.update()
            return

        # --- DRAG ICONA (traslazione) ---
        if self._icon_dragging and \
        self.icon_selected_idx is not None and \
        isinstance(self.icons, list) and \
        0 <= self.icon_selected_idx < len(self.icons):

            # posizione mouse in coordinate widget
            posw = ev.position() if hasattr(ev, "position") else ev.localPos()
            px, py = float(posw.x()), float(posw.y())

            # offset click→angolo TL salvato al mousePress
            offx, offy = getattr(self, "_icon_drag_offset_w", (0.0, 0.0))

            # converti a coordinate canvas (origine al centro)
            cx, cy = self.width()*0.5, self.height()*0.5
            ix = px - offx - cx
            iy = py - offy - cy

            # aggiorna la posizione dell'icona selezionata
            self.icons[self.icon_selected_idx]["pos"] = (ix, iy)

            self.update()
            return
        
        # --- ROTATE BIVIO (punta) ---
        if self._drag_mode == "rotate" and isinstance(self.bivio, dict):
            pos = ev.position() if hasattr(ev, "position") else ev.localPos()
            x = pos.x() - self.width()*0.5
            y = pos.y() - self.height()*0.5
            pv = self.bivio.get("pivot"); pt = self.bivio.get("punta")
            if pv and pt:
                vx, vy = x - pv[0], y - pv[1]
                L = math.hypot(vx, vy)
                if L > 1e-3:
                    ux, uy = vx / L, vy / L
                    nx = pv[0] + ux * self._bivio_len
                    ny = pv[1] + uy * self._bivio_len
                    self.bivio["punta"] = (nx, ny)
                    self.update()
            return

        if self._drag_mode == "translate" and isinstance(self.bivio, dict):
            pos = ev.position() if hasattr(ev, "position") else ev.localPos()
            x = pos.x() - self.width()*0.5
            y = pos.y() - self.height()*0.5

            offx, offy = self._drag_handle_offset
            hx0, hy0 = self._handle_start
            pv0x, pv0y = self._seg_start_pv
            pt0x, pt0y = self._seg_start_pt

            # handle target = mouse - offset
            hx1 = x - offx
            hy1 = y - offy

            # delta da applicare a tutto il segmento
            dx = hx1 - hx0
            dy = hy1 - hy0

            self.bivio["pivot"] = (pv0x + dx, pv0y + dy)
            self.bivio["punta"] = (pt0x + dx, pt0y + dy)
            self.update()
            return

    def mouseReleaseEvent(self, ev):
        # --- fine ROTAZIONE icona ---
        if getattr(self, "_icon_rotating", False):
            self._icon_rotating = False
            self.setMouseTracking(False)
            if callable(self.on_icon_changed):
                try:
                    self.on_icon_changed(self.icons)  # salva su DB se serve
                except Exception:
                    pass
            return
        if self._icon_dragging:
            self._icon_dragging = False
            # usa il nome CORRETTO della callback
            if callable(self.on_icon_changed):
                try:
                    self.on_icon_changed(self.icons)
                except Exception:
                    pass
            return

        if self._drag_mode in ("rotate", "translate"):
            self._drag_mode = None
            self.setMouseTracking(False)
            if callable(self.on_bivio_changed):
                try:
                    self.on_bivio_changed(self.bivio)
                except Exception:
                    pass
            return

    # ---- rendering
    def _pen_for(self, sty):
        pen = QPen(QColor(sty.get("color", "#dcdcdc")))
        th = float(sty.get("thickness", 2.0))
        try:
            pen.setWidthF(th)
        except Exception:
            pen.setWidth(max(1, int(round(th))))
        pen.setCapStyle(Qt.RoundCap)
        pen.setJoinStyle(Qt.RoundJoin)
        if sty.get("dashed", False):
            pen.setStyle(Qt.DashLine)
        return pen
    
    def _draw_road_line(self, painter, x1, y1, x2, y2, style):
        # linea base
        pen = self._pen_for(style)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.drawLine(int(x1), int(y1), int(x2), int(y2))

        # linea centrale bianca se lines==2
        try:
            nlines = int(style.get("lines", 1))
        except Exception:
            nlines = 1

        if nlines == 2:
            base_th = float(style.get("thickness", 2.0) or 2.0)
            # spessore linea centrale (se non specificato: fallback)
            ct = style.get("center_thickness", None)
            try:
                center_th = float(ct) if ct is not None else base_th * 0.5
            except Exception:
                center_th = base_th * 0.5
            center_th = max(1.0, min(center_th, max(1.0, base_th - 1.0)))
            pen2 = QPen(QColor("#ffffff"))
            pen2.setWidthF(center_th)
            pen2.setCosmetic(True)
            pen2.setCapStyle(Qt.RoundCap)
            pen2.setJoinStyle(Qt.RoundJoin)
            painter.setPen(pen2)
            painter.drawLine(int(x1), int(y1), int(x2), int(y2))

    def paintEvent(self, ev):
        if self.b_in is None and self.b_out is None:
            return
        p = QPainter(self); p.setRenderHint(QPainter.Antialiasing, True)

        w, h = self.width(), self.height()
        cx, cy = w*0.5, h*0.5
        L  = max(22, min(w, h) * 0.36)
        R  = 6.0

        p.translate(cx, cy)

        # IN
        self._draw_road_line(p, 0, 0, 0, L, self.style_in)

        # OUT
        if self.b_in is not None and self.b_out is not None:
            def norm180(a):
                while a > 180: a -= 360
                while a <= -180: a += 360
                return a
            d = norm180(float(self.b_out) - float(self.b_in))
            if abs(float(self.b_in) - 180.0) < 1e-6 and abs(float(self.b_out) - 0.0) < 1e-6:
                d = 0.0
            ang = math.radians(d)
            ux, uy = math.sin(ang), -math.cos(ang)
            dx, dy = L*ux, L*uy
            self._draw_road_line(p, 0, 0, dx, dy, self.style_out)
            # punta freccia
            ah, bw = 8.0, 8.0
            lx, ly = -uy, ux
            bx, by = dx - ux*ah, dy - uy*ah
            poly = QPolygon([
                QPoint(int(dx), int(dy)),
                QPoint(int(bx + lx*bw/2), int(by + ly*bw/2)),
                QPoint(int(bx - lx*bw/2), int(by - ly*bw/2)),
            ])
            p.setBrush(QColor(self.style_out.get("color", "#dcdcdc")))
            p.setPen(self._pen_for(self.style_out))
            p.drawPolygon(poly)

            # --- BIVI: disegna tutti (maniglie solo sul selezionato) ---
            items = self.bivi_lista if isinstance(self.bivi_lista, list) else ([self.bivio] if isinstance(self.bivio, dict) else [])
            for _b in items:
                pv = _b.get("pivot"); pt = _b.get("punta")
                if not (isinstance(pv, (tuple, list)) and isinstance(pt, (tuple, list)) and len(pv) == 2 and len(pt) == 2):
                    continue

                pen_b = QPen(QColor("#9e9e9e"))
                th = float(_b.get("th", 2.0))
                try:
                    pen_b.setWidthF(th)
                except Exception:
                    pen_b.setWidth(int(round(th)))
                pen_b.setCosmetic(True)
                pen_b.setCapStyle(Qt.RoundCap); pen_b.setJoinStyle(Qt.RoundJoin)
                p.setPen(pen_b); p.setBrush(Qt.NoBrush)

                # segmento principale
                p.drawLine(int(pv[0]), int(pv[1]), int(pt[0]), int(pt[1]))

                # riga bianca SOLO se il BIVIO è autostrada/asfalto (rt -> road_types[rt]["lines"] == 2)
                rt = _b.get("rt", None)

                # fallback intelligente per bivi vecchi senza rt: deduco da spessore
                if rt is None:
                    if th >= 10:
                        rt = 1       # autostrada
                    elif th >= 6:
                        rt = 2       # asfalto
                    else:
                        rt = 0       # poco visibile / pista
                try:
                    rt = int(rt)
                except Exception:
                    rt = 0

                if rt < 0 or rt >= len(road_types):
                    rt = 0

                try:
                    biv_lines = int(road_types[rt].get("lines", 1))
                except Exception:
                    biv_lines = 1

                if biv_lines == 2:
                    ct = road_types[rt].get("center_thickness", None)
                    try:
                        center_th = float(ct) if ct is not None else (th * 0.5)
                    except Exception:
                        center_th = th * 0.5
                    center_th = max(1.0, center_th)

                    pen_w = QPen(QColor("#ffffff"))
                    pen_w.setWidthF(center_th)
                    pen_w.setCosmetic(True)
                    pen_w.setCapStyle(Qt.RoundCap)
                    pen_w.setJoinStyle(Qt.RoundJoin)
                    p.setPen(pen_w)
                    p.drawLine(int(pv[0]), int(pv[1]), int(pt[0]), int(pt[1]))

                    # ripristina pen grigio per i trattini e il resto
                    p.setPen(pen_b)

                # trattini vicino alla punta
                b_dx, b_dy = pt[0] - pv[0], pt[1] - pv[1]
                b_L = math.hypot(b_dx, b_dy)
                if b_L > 1e-6:
                    b_ux, b_uy = b_dx / b_L, b_dy / b_L
                    b_tx, b_ty = -b_uy, b_ux
                    rt = int(_b.get("rt", 0) or 0)
                    is_double = rt in (1, 2)   # autostrada o asfalto

                    if is_double:
                        Ltick1, Ltick2 = 9, 11
                    else:
                        Ltick1, Ltick2 = 6, 8

                    off1, off2 = 2, 8
                    cx1, cy1 = pt[0] - b_ux * off1, pt[1] - b_uy * off1
                    cx2, cy2 = pt[0] - b_ux * off2, pt[1] - b_uy * off2

                    # pen più sottile SOLO per i trattini
                    pen_tick = QPen(QColor("#9e9e9e"))
                    pen_tick.setWidthF(3.0)          # prova 2.0 (se vuoi più fine: 1.5)
                    pen_tick.setCosmetic(True)
                    pen_tick.setCapStyle(Qt.RoundCap)
                    pen_tick.setJoinStyle(Qt.RoundJoin)
                    p.setPen(pen_tick)

                    p.drawLine(int(cx1 - b_tx * Ltick1), int(cy1 - b_ty * Ltick1),
                               int(cx1 + b_tx * Ltick1), int(cy1 + b_ty * Ltick1))
                    p.drawLine(int(cx2 - b_tx * Ltick2), int(cy2 - b_ty * Ltick2),
                               int(cx2 + b_tx * Ltick2), int(cy2 + b_ty * Ltick2))

                    p.setPen(pen_b)  # ripristina pen del bivio


                    # maniglie SOLO sul selezionato
                    if self.bivio_selected and (_b == self.bivio):
                        r = 5
                        p.setBrush(QColor("#ffffff"))
                        p.setPen(QPen(QColor("#1976D2"), 2)); p.drawEllipse(QPoint(int(pv[0]), int(pv[1])), r, r)  # pivot (blu)
                        p.setPen(QPen(QColor("#B71C1C"), 2)); p.drawEllipse(QPoint(int(pt[0]), int(pt[1])), r, r)  # punta (rosso)

                        # maniglia di traslazione sulla linea (sotto ai trattini)
                        extra = 7.0
                        base  = max(18.0, min(b_L - 6.0, 0.35 * b_L))
                        offH  = min(b_L - 6.0, base + extra)
                        hx = pt[0] - b_ux * offH
                        hy = pt[1] - b_uy * offH
                        p.setPen(QPen(QColor("#212121"), 2)); p.setBrush(QColor("#FAFAFA"))
                        p.drawEllipse(QPoint(int(hx), int(hy)), r, r)

        # marker OUT se selezionato
        if self.is_selected:
            mark_len = 12.0
            lx, ly = -uy, ux
            mx, my = dx*0.5, dy*0.5
            pen_mark = QPen(QColor("#e53935"))
            pen_mark.setWidth(max(2, int(round(float(self.style_out.get("thickness", 2.0))))))
            pen_mark.setCapStyle(Qt.RoundCap)
            p.setPen(pen_mark)
            p.drawLine(int(mx - lx*mark_len/2), int(my - ly*mark_len/2),
                       int(mx + lx*mark_len/2), int(my + ly*mark_len/2))
            
        # --- ICONE: disegna tutte ---
        self._icon_rects_widget = []  # reset dei rect per l'hit-test ad ogni paint
        items = self.icons if isinstance(self.icons, list) else []
        if items:
            margin = 8
            top_left_x = -w*0.5 + margin
            top_left_y = -h*0.5 + margin

            # AZZERA gli hit-test ad ogni paint per evitare rettangoli “stali”
            self._icon_rects_widget = []
            self._icon_rot_rects_widget = []
            self._icon_rot_handles_widget = []

            for i, ic in enumerate(items):
                fpath = ic.get("file")
                if not fpath:
                    continue
                try:
                    pm = QPixmap(fpath)
                except Exception:
                    pm = QPixmap()
                if pm.isNull():
                    continue

                angle = float(ic.get("angle", 0.0) or 0.0)
                size = max(28, int(round(min(w, h) * 0.12)))  # dimensione icone

                # ↓ solo per la rotaia: riducila un po'
                if os.path.basename(fpath) == "P06_ferrovia.png":
                    size = int(round(size * 0.6))

                # ridimensiona mantenendo proporzioni usando il LATO CORTO come riferimento
                w_nat = max(1, pm.width())
                h_nat = max(1, pm.height())

                # ridimensiona mantenendo proporzioni usando il LATO CORTO come riferimento
                w_nat = max(1, pm.width())
                h_nat = max(1, pm.height())

                # (opzionale) segnali quadrati un po' più grandi
                base = os.path.basename(fpath).lower()
                if re.match(r"^s\d+", base) and abs(w_nat - h_nat) <= 2:   # segnale + quasi quadrato
                    size = int(round(size * 1.20))  # 1.10/1.15/1.25 a piacere

                short = min(w_nat, h_nat)
                scale = float(size) / float(short)
                tw = int(round(w_nat * scale))
                th = int(round(h_nat * scale))
                pm = pm.scaled(tw, th, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)

                # posizione (default: sempre in alto a sinistra)
                pos = ic.get("pos")
                if isinstance(pos, (tuple, list)) and len(pos) == 2:
                    x, y = float(pos[0]), float(pos[1])
                else:
                    x, y = top_left_x, top_left_y

                # disegno (supporto flipX)
                flip = bool(ic.get("flipX", False))
                p.save()
                p.translate(x, y)
                if angle:
                    p.rotate(angle)
                if flip:
                    p.scale(-1, 1)
                    p.drawPixmap(-pm.width(), 0, pm)   # disegna “specchiato” attorno al TL
                else:
                    p.drawPixmap(0, 0, pm)

                # bordo arancione se selezionata
                if self.icon_selected_idx == i:
                    p.setPen(QPen(QColor("#FF9800"), 2, Qt.DashLine))
                    p.setBrush(Qt.NoBrush)
                    rect_x = -pm.width() if bool(ic.get("flipX", False)) else 0
                    p.drawRect(rect_x, 0, pm.width(), pm.height())

                    # --- maniglia di ROTAZIONE (in alto a destra) ---
                    hx_local = 0 if bool(ic.get("flipX", False)) else pm.width()
                    RH = 5                    
                    p.setPen(QPen(QColor("#B71C1C"), 2))   # bordo rosso
                    p.setBrush(QColor("#B71C1C"))          # riempimento rosso
                    p.drawEllipse(QPoint(hx_local, 0), RH, RH)

                p.restore()

                # bounding-rect (in coord. widget) usando lo stesso transform del draw
                iw, ih = pm.width(), pm.height()
                flip = bool(ic.get("flipX", False))

                t = QTransform()
                t.translate(self.width()*0.5, self.height()*0.5)  # centro canvas
                t.translate(x, y)
                if angle:
                    t.rotate(angle)
                if flip:
                    t.scale(-1, 1)

                # rettangolo locale esattamente come nel draw (flip ⇒ parte da -iw,0)
                local_rect = QRectF(-iw if flip else 0, 0, iw, ih)
                rect_w = t.mapRect(local_rect).toRect()

                # salva rect per l'hit-test
                if i >= len(self._icon_rects_widget):
                    self._icon_rects_widget.extend([QRect()] * (i + 1 - len(self._icon_rects_widget)))
                self._icon_rects_widget[i] = rect_w

                # maniglia di rotazione: punto locale (top-right visivo) → widget
                hr_local = QPointF(0 if flip else iw, 0)
                hr_widget = t.map(hr_local)
                RH = 5
                rot_rect = QRect(int(hr_widget.x() - RH), int(hr_widget.y() - RH), RH*2, RH*2)

                if i >= len(self._icon_rot_rects_widget):
                    self._icon_rot_rects_widget.extend([QRect()] * (i + 1 - len(self._icon_rot_rects_widget)))
                self._icon_rot_rects_widget[i] = rot_rect if (self.icon_selected_idx == i) else QRect()

                if i >= len(self._icon_rot_handles_widget):
                    self._icon_rot_handles_widget.extend([(0, 0)] * (i + 1 - len(self._icon_rot_handles_widget)))
                self._icon_rot_handles_widget[i] = (hr_widget.x(), hr_widget.y())

                # handle rotazione = top-right VISIVO (in locali → ruotato → widget)
                x0 = -iw if bool(ic.get("flipX", False)) else 0
                hr_local_x, hr_local_y = (x0+iw, 0)
                rad = math.radians(angle or 0.0); c, s = math.cos(rad), math.sin(rad)
                hx_r = hr_local_x*c - hr_local_y*s
                hy_r = hr_local_x*s + hr_local_y*c
                HXw, HYw = hx_r + x + cx, hy_r + y + cy
                RH = 5
                rot_rect = QRect(int(HXw - RH), int(HYw - RH), RH*2, RH*2)

                if i >= len(self._icon_rot_handles_widget):
                    self._icon_rot_handles_widget.extend([(0, 0)] * (i + 1 - len(self._icon_rot_handles_widget)))
                self._icon_rot_handles_widget[i] = (HXw, HYw)

                # garantisci la lunghezza delle liste e salva
                if i >= len(self._icon_rects_widget):
                    self._icon_rects_widget.extend([QRect()] * (i + 1 - len(self._icon_rects_widget)))
                self._icon_rects_widget[i] = rect_w

                # hotspot circolare per la maniglia rossa (angolo alto-destro ruotato)
                RH = 5
                hx_l, hy_l = pm.width(), 0             # top-right in locale icona
                hx_r = hx_l*c - hy_l*s;  hy_r = hx_l*s + hy_l*c
                HXw = hx_r + x + cx;     HYw = hy_r + y + cy
                rot_rect = QRect(int(HXw - RH), int(HYw - RH), RH*2, RH*2)

                # salva anche il centro della maniglia (utile per calcolare l’angolo)
                if i >= len(self._icon_rot_handles_widget):
                    self._icon_rot_handles_widget.extend([(0, 0)] * (i + 1 - len(self._icon_rot_handles_widget)))
                self._icon_rot_handles_widget[i] = (HXw, HYw)

        # cerchietto centrale
        p.setPen(QPen(QColor("#222"))); p.setBrush(QColor("#fff"))
        p.drawEllipse(QPoint(0, 0), int(R), int(R))
        p.end()

    #                                                                                                            ICONE
    def set_icons(self, icons_list):
        """Imposta l'elenco icone da disegnare (lista di dict)."""
        self.icons = icons_list if isinstance(icons_list, list) else []
        self.update()

    def add_icon(self, icon_dict):
        """Aggiunge un'icona (dict) e ridisegna."""
        if not isinstance(self.icons, list):
            self.icons = []
        if isinstance(icon_dict, dict):
            self.icons.append(icon_dict)
            self.update()

    def keyPressEvent(self, ev):
        if ev.key() == Qt.Key_H:
            # flip orizzontale dell’icona selezionata
            if isinstance(self.icons, list) and self.icon_selected_idx is not None \
            and 0 <= self.icon_selected_idx < len(self.icons):
                ic = self.icons[self.icon_selected_idx]
                ic["flipX"] = not bool(ic.get("flipX", False))
                self.update()
                # salva subito su database_traccia
                if callable(self.on_icon_changed):
                    try:
                        self.on_icon_changed(self.icons)
                    except Exception:
                        pass
            ev.accept()
            return
        super().keyPressEvent(ev)


# ---                                                                                                     LAYOUT DELLE NOTE
class NoteRow(QFrame):
    def __init__(self, num, km_prog, km_parz, fonts, sizes, on_click_a):
        super().__init__()
        self.setObjectName("note-row")
        self.setFrameShape(QFrame.NoFrame)
        self.setFixedHeight(sizes['note_h'])
        
        # separatore sotto tutta la riga
        self.setStyleSheet("#note-row { border-bottom: 1px solid #bdbdbd; }")


        # -- helpers ----------------------------------------------------------
        from PySide6.QtGui import QFont

        def _font_of(v, weight=QFont.Normal):
            """Accetta QFont oppure un numero (pt)."""
            if isinstance(v, QFont):
                f = QFont(v)  # copia
            else:
                f = QFont()
                try: f.setPointSize(int(v))
                except Exception: pass
                f.setWeight(weight)
            # nitidezza
            try:
                f.setHintingPreference(QFont.PreferFullHinting)
                f.setStyleStrategy(QFont.PreferAntialias)
            except Exception:
                pass
            return f
        # --------------------------------------------------------------------

        h = QHBoxLayout(self)
        h.setContentsMargins(0, 0, 0, 1)
        h.setSpacing(0)

        # -- Colonna A --------------------------------------------------------
        colA = QFrame()
        colA.setObjectName("col-a")
        colA.setFixedWidth(sizes['col_a_w'])
        colA.setFrameShape(QFrame.NoFrame)
        la = QVBoxLayout(colA)
        la.setContentsMargins(8, 6, 8, 6)
        la.setSpacing(0)

        lbl_prog = QLabel(f"{km_prog:.2f}")
        lbl_prog.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        lbl_prog.setFont(_font_of(fonts['prog'], QFont.Bold))

        bottom = QHBoxLayout()
        bottom.setContentsMargins(0, 0, 0, 0)
        bottom.setSpacing(6)

        lbl_parz = QLabel(f"{km_parz:.2f}")
        lbl_parz.setAlignment(Qt.AlignLeft | Qt.AlignBottom)
        lbl_parz.setFont(_font_of(fonts['parz']))

        # numero nota SENZA zeri davanti
        box_num = QLabel(str(num))
        box_num.setAlignment(Qt.AlignCenter)
        box_num.setFont(_font_of(fonts['num'], QFont.DemiBold))
        box_num.setFixedWidth(54)
        box_num.setStyleSheet("QLabel { border:1px solid #444; border-radius:4px; padding:2px 6px; }")
        self.box_num = box_num

        bottom.addWidget(lbl_parz, 1)
        bottom.addWidget(box_num, 0)

        la.addWidget(lbl_prog, 1)

        la.addLayout(bottom)

        # -- Sep verticale 1 --------------------------------------------------
        sep1 = QFrame()
        sep1.setFixedWidth(2)
        sep1.setStyleSheet("background:#dddddd;")
        self.sep1 = sep1

        # -- Colonna B (vettori direzione) -----------------------------------
        self.canvasB = VectorCanvas()
        self.canvasB.setFixedWidth(sizes['col_b_w'])
        colB = self.canvasB

        # -- Sep verticale 2 --------------------------------------------------
        sep2 = QFrame()
        sep2.setFixedWidth(2)
        sep2.setStyleSheet("background:#dddddd;")
        self.sep2 = sep2

        # -- Colonna C (layout base) -----------------------------------------
        colC = QFrame()
        colC.setStyleSheet("background:transparent;")
        lc = QVBoxLayout(colC)
        lc.setContentsMargins(10, 6, 10, 6)
        lc.setSpacing(4)

        # slot fisso per CAP rosso (spazio riservato)
        cap_slot = QFrame()
        cap_slot.setFixedHeight(int(fonts.get('cap_hdr_h', 32)))
        cap_slot.setStyleSheet("background:transparent; margin:0; padding:0;")
        cap_slot_l = QHBoxLayout(cap_slot)
        cap_slot_l.setContentsMargins(0, 0, 0, 0)
        cap_slot_l.setSpacing(0)

        cap_hdr = QLabel("")  # vuoto finché non premi CAP
        cap_hdr.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        cap_hdr.setStyleSheet("color:#e60000;")

        # font con scala robusta
        try:
            base_pt = int(fonts.get('prog', 18))
        except Exception:
            base_pt = 18
        scale = float(fonts.get('cap_hdr_scale', 1.0))
        target_pt = max(8, int(round(base_pt * scale)))
        f_hdr = _font_of(base_pt, QFont.Bold)
        try: f_hdr.setPointSize(target_pt)
        except Exception: pass
        cap_hdr.setFont(f_hdr)

        cap_slot_l.addWidget(cap_hdr)
        lc.addWidget(cap_slot, 0)

        # riferimenti
        self.cap_hdr  = cap_hdr
        self.cap_slot = cap_slot
        cap_hdr.setVisible(False)  # verrà mostrato quando premi il bottone CAP

        # testo centrale: 2 righe, wrap, read-only (poi lo abilitiamo con T)
        desc = QTextEdit("")
        desc.setObjectName("DescEdit")
        desc.setAcceptRichText(False)
        desc.setReadOnly(True)
        desc.setPlaceholderText("")  # niente placeholder
        desc.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
        desc.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        desc.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        desc.setFrameShape(QFrame.NoFrame)
        desc.setAlignment(Qt.AlignHCenter)

        opt = desc.document().defaultTextOption()
        opt.setAlignment(Qt.AlignHCenter)
        desc.document().setDefaultTextOption(opt)

        # font leggermente più piccolo del 'parz'
        f_txt = _font_of(fonts.get('parz'))
        try:
            ps = f_txt.pointSize()
            if ps > 0: f_txt.setPointSize(max(8, ps - 2))
        except Exception:
            pass
        desc.setFont(f_txt)

        # limite a 37 caratteri (live) e niente newline
        MAXLEN = int(fonts.get('text_max_len', 37))
        def _enforce_maxlen():
            t = desc.toPlainText().replace("\n", " ")
            if len(t) > MAXLEN:
                pos = desc.textCursor().position()
                desc.blockSignals(True)
                desc.setPlainText(t[:MAXLEN])
                # ripristina il cursore dove era (finché possibile)
                c = desc.textCursor()
                c.setPosition(min(pos, MAXLEN))
                desc.setTextCursor(c)
                desc.blockSignals(False)

        desc.textChanged.connect(_enforce_maxlen)

        # altezza per 2 righe (senza scroll)
        fm = desc.fontMetrics()
        h2 = fm.lineSpacing() * 2 + 8
        desc.setFixedHeight(max(int(fonts.get('text_min_h', 72)), h2))

        lc.addWidget(desc, 0)

        # riferimenti compatibili col resto del codice
        self.desc_edit = desc
        self.desc_lbl  = desc  # alias, così niente si rompe

        # separatore basso
        sep_bot = QFrame(); sep_bot.setFixedHeight(1); sep_bot.setStyleSheet("background:#222;")
        lc.addWidget(sep_bot, 0)

        # riga info: CAP in basso-sx, coordinate in basso-dx
        row_info = QHBoxLayout(); row_info.setContentsMargins(0,0,0,0); row_info.setSpacing(12)
        cap_small = QLabel("60º")
        cap_small.setFont(_font_of(fonts.get('cap_btm', fonts['parz'])))
        # riduci un po' la dimensione (robusto anche se fonts è solo numero)
        try:
            f = cap_small.font(); f.setPointSize(max(9, f.pointSize() - 3)); cap_small.setFont(f)
        except Exception:
            cap_small.setStyleSheet((cap_small.styleSheet() + "; font-size: 11pt;").strip(";"))

        latlon = QLabel("—    —")
        latlon.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        latlon.setFont(_font_of(fonts.get('coord_btm', fonts['parz'])))
        try:
            f2 = latlon.font(); f2.setPointSize(max(9, f2.pointSize() - 4)); latlon.setFont(f2)
        except Exception:
            latlon.setStyleSheet((latlon.styleSheet() + "; font-size: 10pt;").strip(";"))

        row_info.addWidget(cap_small, 0)
        row_info.addStretch(1)
        row_info.addWidget(latlon, 0)
        lc.addLayout(row_info)

        # riferimenti usati dal MainWindow
        self.cap_hdr = cap_hdr
        self.desc_lbl = desc
        self.cap_small = cap_small
        self.coord_lbl = latlon

        # A e B fissi (evitano di rubare spazio a C)
        colA.setMinimumWidth(120)
        colA.setMaximumWidth(120)
        colA.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)

        colB.setMinimumWidth(220)
        colB.setMaximumWidth(220)
        colB.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)

        # separatori sempre larghi 1
        sep1.setFixedWidth(1)
        sep2.setFixedWidth(1)

        # C elastica ma con un minimo visibile
        colC.setMinimumWidth(260)
        colC.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

        h.addWidget(colA)
        h.addWidget(sep1)
        h.addWidget(colB)
        h.addWidget(sep2)
        h.addWidget(colC, 1)

        # Click su colonna A → centra waypoint associato
        colA.mousePressEvent = lambda ev: on_click_a(num)

        # per evidenze
        self.colA = colA

    #                                                                                 EVIDENZIA IL NUMERO NOTA ATTIVA CAMBIANDO LOS FONDO DEL CAMPO IN ROSSO
    def set_recent(self, recent: bool):
        if recent:
            self.box_num.setStyleSheet(
                "QLabel { border:2px solid #ff9800; border-radius:4px; padding:2px 6px; "
                "background:#fff3e0; color:#e65100; }"
            )
        else:
            self.box_num.setStyleSheet(
                "QLabel { border:1px solid #444; border-radius:4px; padding:2px 6px; }"
            )
    #                                                                                                  EVIDENCIA L'INTERA NORTA SE SELEZIONATA
    def set_selected(self, selected: bool):
        """Evidenza gialla sull’intera riga."""
        self.setStyleSheet("#note-row { background:#fff9c4; border-bottom:1px solid #bdbdbd; }" if selected
                   else "#note-row { border-bottom:1px solid #bdbdbd; }")

    #                                                                                        EVIDENZIA LA COLONNA A SE IL PARZIALE È INFERIORE A 250 METRI
    def set_short(self, is_short: bool):
        """Azzurrino sulla sola colonna A se parziale corto."""
        self.colA.setStyleSheet("background:#e3f2fd;" if is_short else "background:transparent;")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MainWindow()
    w.showMaximized()
    sys.exit(app.exec())
